Vercel Cron can fire twice: key the Sume submit by schedule slot

Vercel cron delivery is best effort and never retried. Derive the Sume Idempotency-Key from the schedule slot so a repeat adopts the first job.

4 min readSume
All posts

A Vercel cron route that submits a paid Sume job should send an Idempotency-Key built from the schedule slot, for example the UTC date, not a random UUID. Vercel documents that a scheduled run can occasionally be delivered more than once, and a failed run is never retried, so the same slot has to be safe to submit twice and safe to submit late.

With a slot key, a duplicate delivery returns the original Sume job with idempotency_hit: true instead of creating and billing a second one. A missed run is handled the other way: a second, later schedule entry submits the same slot key and either creates the job or adopts the one that already exists.

What Vercel promises and what it does not

The cron page is explicit about the failure modes. Both the missed run and the duplicate run are described as normal, and the recommended design is reconciliation: each run should be able to reprocess outstanding work since the last success. For a paid generation call, the cheapest form of reconciliation is a deterministic key.

Sume's side of the contract is in the API reference: reusing a key with the same operation and payload returns the original job, and a 5xx never proves the work was refused. A random UUID per invocation throws that protection away, because every duplicate looks like new work.

Cron behavior that shapes the handler (read 2026-10-03)
TopicVercel cronWhat the Sume submit should do
DeliveryBest effort; transient network errors can skip a runRun a second schedule later in the day with the same slot key
DuplicatesSame scheduled run can occasionally invoke twiceSlot key makes the second call an idempotent replay
RetriesNo retry when the invocation failsRetry inside the handler, same key
AuthCRON_SECRET is sent as an Authorization Bearer headerRefuse the call when the secret is unset or does not match
Hobby accuracyOnce per day, any time within the scheduled hourKey by date, never by minute
RedirectsNot followed; a 3xx is finalRegister the exact route path

A route handler that is safe to run twice

The handler below checks CRON_SECRET the way the Vercel page shows (and refuses to run when the variable is missing), submits an async image job, and returns the Sume job id. It sends no mode other than async, so the cron invocation returns in well under a function limit and never waits on generation. Read the result later with a webhook or a poll, as in the Jobs and results guide.

// app/api/cron/hero/route.ts
export async function GET(req: Request) {
  const secret = process.env.CRON_SECRET;
  if (!secret || req.headers.get("authorization") !== `Bearer ${secret}`) {
    return new Response("Unauthorized", { status: 401 });
  }
  const slot = new Date().toISOString().slice(0, 10); // one job per UTC day
  const res = await fetch("https://api.sume.com/v1/images", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SUME_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `daily-hero-${slot}`,
    },
    body: JSON.stringify({
      model: "sume/auto",
      prompt: "Minimal desk scene, soft morning light, no text",
      mode: "async",
    }),
  });
  const body = await res.json();
  if (!res.ok) return Response.json(body, { status: 502 });
  return Response.json({ job_id: body.data.job.id, replay: body.data.idempotency_hit });
}

Choosing the slot

The slot should name the intent, not the clock reading. A daily hero image is one intent per UTC date, so the date is the slot. An hourly job would use the date plus the hour. Avoid including minutes or seconds: on a Hobby plan Vercel may fire anywhere inside the scheduled hour, and a key that contains the minute would differ between a run and its duplicate.

If the prompt itself changes between the first and second delivery, Sume answers 409 idempotency_conflict because the payload no longer matches the key. The key is already held by the first job, so treat the conflict as proof that the slot was submitted. Keep the prompt a pure function of the slot and the conflict never happens.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume