Set your Format run timeout from expires_at, not a guess
A non-terminal Format run receipt carries expires_at, the deadline after which Sume force-finalizes it as failed. Derive your wait from it and read queue.state.

Take your wait budget from expires_at on the run receipt. It is the deadline after which Sume force-finalizes a run as failed. The deadline is 90 minutes from created_at, or earlier when the run is older than 25 minutes and has been silent for 10. Once the run is terminal, expires_at is null. The docs tell you to use this value and not to invent a timeout of your own.
Why a fixed client timeout goes wrong
A Format run is long work. Video Formats usually run 10 to 20 minutes, and long-form is 15 to 30. If your client waits a fixed 5 minutes it gives up on healthy runs. If it waits 2 hours it keeps a worker idle on a run that Sume has already ended. A client timeout does not cancel anything either: the run keeps executing and spending after your code gives up. The only value that matches Sume's own clock is expires_at.
| Field on a non-terminal receipt | Meaning |
|---|---|
expires_at | Deadline for force-finalizing the run as failed |
queue.state: waiting | Pickup is inside the normal window |
queue.state: runtime_unavailable | Waited longer than the window and nothing claimed the run |
queue.retry_after_seconds | How long to back off in runtime_unavailable |
queue.position | Always null. Sume does not publish queue depth |
Compute the budget
The helper turns expires_at into milliseconds left and picks the next poll delay from queue. The sample runs offline against a made-up receipt, with a fixed clock so the output is stable. In real use, pass the receipt you just read.
// Bound your own wait with the receipt's expires_at instead of inventing a timeout.
export function waitBudgetMs(receipt, now = Date.now()) {
if (!receipt.expires_at) return 0; // null once the run is terminal
return Math.max(0, Date.parse(receipt.expires_at) - now);
}
export function nextDelaySeconds(status) {
const q = status.queue;
if (q?.state === "runtime_unavailable") return q.retry_after_seconds ?? 30;
return 5; // waiting: normal pickup window
}
const created = Date.parse("2026-10-07T00:00:00.000Z");
const receipt = { expires_at: new Date(created + 90 * 60_000).toISOString() };
console.log(waitBudgetMs(receipt, created + 30 * 60_000) / 60_000, "minutes left");
console.log(nextDelaySeconds({ queue: { state: "runtime_unavailable", retry_after_seconds: 45 } }));Checking the numbers
The sample builds a receipt that was created at a fixed time and asks how much of its 90 minutes is left 30 minutes in. The answer printed is 60 minutes. The same arithmetic works on a live receipt, except that the earlier cut-off also applies: a run older than 25 minutes that went silent for 10 can end before the 90 minute mark, and the receipt's expires_at already reflects the date Sume will use. Read it again on each poll rather than caching the first value.
What each branch means
waitBudgetMsis zero orexpires_atisnull: the run is terminal or about to be. Readstatus_urlonce more and stop waiting.runtime_unavailablefor a few minutes is worth a log line. If it lasts more than a few minutes, the docs ask for a support ticket with therequest_id.- Do not display
queue.position. It is alwaysnull, so a queue-position bar has nothing to show. - A
429or503while polling does not moveexpires_at. The run did not fail; your read did.
With the SDK
waitForRun has a 10 minute default timeout and throws SumeRunTimeoutError with runId and lastStatus when it fires. For long-form video pass a larger timeout. The helper checks the deadline before it sleeps, so a 5-second timeout returns in 5 seconds, not 5 seconds plus a poll interval. When the error arrives, the run is still live: resume with the runId instead of creating a second run, which would bill twice.
A webhook skips all of this. Send communication.webhook_url and the terminal delivery arrives once when the run completes or fails. Keep a poll as a backup. See Runs and results for the poll rules and SDK runs for waitForRun.
Sources
Related posts
More in Developers
- Format run stuck on processing: stalled or slow? Read events_url
A Format run can show processing for a long time. The last at value on events_url is the progress clock: if it stops moving for minutes, the run is stalled.
- GET result_url returns 409 run_not_completed: poll status_url first
A 409 run_not_completed from result_url means the Format run is not terminal. Read details.status, poll status_url with backoff, then fetch the result.
- 40 Omni clips, 3 fail, 2 canceled while queued: where the wallet ends
Forty 8-second Omni clips at 720p reserve $40.00. With 3 failures and 2 cancels before start, the wallet captures $35.00.
- generation_spend_cap_usd on a Format run: null is $500, 0 is a 400
On a Format run request, omit generation_spend_cap_usd for the Format cap, send a number up to 500, null for the $500 maximum. 0 or above 500 returns 400.
Written by Sume