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.

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.
| Topic | Vercel cron | What the Sume submit should do |
|---|---|---|
| Delivery | Best effort; transient network errors can skip a run | Run a second schedule later in the day with the same slot key |
| Duplicates | Same scheduled run can occasionally invoke twice | Slot key makes the second call an idempotent replay |
| Retries | No retry when the invocation fails | Retry inside the handler, same key |
| Auth | CRON_SECRET is sent as an Authorization Bearer header | Refuse the call when the secret is unset or does not match |
| Hobby accuracy | Once per day, any time within the scheduled hour | Key by date, never by minute |
| Redirects | Not followed; a 3xx is final | Register 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
- Vimeo embed captions on by default: texttrack and cc parameters
Add texttrack=en to a Vimeo embed URL to open with captions on. What cc does, when texttrack is ignored, and when a burned-in caption is the safer fix.
- VS Code --add-mcp for a remote server: add Sume with mcp.json
VS Code documents code --add-mcp only with a local command. For a remote server like Sume, put a type http entry in mcp.json and sign in with OAuth.
- VS Code Agent Host skips .vscode/mcp.json inputs: place Sume's entry
VS Code's Agent Host forwards your MCP config but drops entries that need input variables. Where to put Sume's entry and how to pass an API key without them.
- VS Code sandboxEnabled for MCP: does it cover a hosted Sume entry?
VS Code's MCP sandbox covers local stdio servers on macOS and Linux. A hosted Sume HTTP entry is outside it; scopes and spend limits are the gates.
Written by Sume