How long to poll a Format run: use expires_at, not your own timeout

A non-terminal Format run receipt carries expires_at, 90 minutes after created_at or earlier if the run goes silent. Set your poll ceiling from it and back off.

5 min readSume
All posts

Poll a Format run until its status is terminal or until its expires_at, whichever comes first, and do not invent a timeout of your own. A non-terminal receipt carries expires_at: the deadline after which Sume force-finalizes the run as failed. It is 90 minutes from created_at, or earlier when the run is older than 25 minutes and has been silent for 10.

The rules for a correct loop

The Runs docs give three practices. Back off, because long-form video is 15 to 30 minutes of work and a poll each second gives nothing and uses read budget. Use expires_at as the ceiling. Read queue if the run stays queued.

  • Start at 5 seconds and double the gap up to 60 seconds; the docs' own loop does exactly that.
  • A 429 or 503 during the loop is temporary. The run keeps executing and spending, so wait and poll again; do not mark it failed.
  • When the run is terminal, expires_at is null.
  • If queue.state is runtime_unavailable, retry_after_seconds tells you how long to back off; if it lasts more than a few minutes, contact support with the request_id.

Time arithmetic

With the 5-second start and the doubling rule, the waits are 5, 10, 20, 40, then 60 seconds each. The first five polls cover 5 + 10 + 20 + 40 + 60 = 135 seconds, and then you poll once a minute. A 90-minute run is 5,400 seconds, so after the first 135 seconds you would make about (5,400 - 135) / 60 = 87.75, so roughly 88 more reads. Reads have a separate budget that is forty times the write budget, so even a Free plan's 4,800 reads a minute is not near this.

Polling cost for one run held for the full 90 minutes, arithmetic as of 2026-10-09
PhaseWaitsReads
Ramp-up5, 10, 20, 40, 60 s5 reads in 135 s
Steady60 s eachabout 88 reads in the remaining 5,265 s
Total90 minutes = 5,400 sabout 93 reads

Better: poll the small endpoint, or take the webhook

status_url returns a small payload with status, next_action, cancelable and expires_at, and never the output. Use it for the loop, then read result_url once at the end. Or send communication.webhook_url and keep the poll as a backup for the day your endpoint is down. In TypeScript, subscribeFormatRun and waitForRun run this loop for you.

What the force-finalize looks like

When the deadline passes, Sume finalizes the run as failed. The artifacts the run already made stay on the receipt, and a failed run that left work can be continued with previous_run_id. So a timeout in your own code is a worse choice than waiting for Sume's: if you give up at 20 minutes, you may drop a run that would have finished at 25.

Use GET /v1/format-runs/{run_id}/events if you want to know whether a slow run is slow or stalled. It shows the phases preparing, running and finalizing, and the at of the last entry is the progress clock. If it has not moved for several minutes, the run is stalled, and Sume will finalize it at the bounds above. The endpoint is a phase timeline, not a log stream, and it will not show tool calls.

Sources

Related posts

More in Formats

All Formats posts

Written by Sume