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.

5 min readSume
All posts

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 receiptMeaning
expires_atDeadline for force-finalizing the run as failed
queue.state: waitingPickup is inside the normal window
queue.state: runtime_unavailableWaited longer than the window and nothing claimed the run
queue.retry_after_secondsHow long to back off in runtime_unavailable
queue.positionAlways 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

  • waitBudgetMs is zero or expires_at is null: the run is terminal or about to be. Read status_url once more and stop waiting.
  • runtime_unavailable for a few minutes is worth a log line. If it lasts more than a few minutes, the docs ask for a support ticket with the request_id.
  • Do not display queue.position. It is always null, so a queue-position bar has nothing to show.
  • A 429 or 503 while polling does not move expires_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

All Developers posts

Written by Sume