Format run expires_at: a 90 minute ceiling for your Python poll loop

A Sume Format run receipt has an expires_at deadline: 90 minutes from creation, sooner if the run goes quiet. Use it as your loop ceiling, with Python backoff.

5 min readSume
All posts

A non-terminal Sume Format run receipt carries an expires_at timestamp, and that is the ceiling for your own polling loop. It is 90 minutes after created_at, or earlier if the run is older than 25 minutes and has been silent for 10. After it passes, Sume force-finalizes the run as failed. Once the run is terminal, expires_at is null. Read it instead of inventing a timeout.

The rule is on the Runs and results page, read 2026-10-09. It matters for long-form video Formats, which the page says can be 15 to 30 minutes of work, so a loop that gives up at 5 minutes abandons a run that is still spending.

What the poll gives you

The receipt holds its own URLs. Use them, as the page advises, and do not build paths by hand.

Format run poll fields, as of 2026-10-09 (Runs and results).
FieldMeaning
statusqueued and processing keep going; terminal values end the loop
expires_atDeadline after which Sume finalizes the run as failed; null when terminal
queue.statewaiting in the normal pickup window; runtime_unavailable when nothing claimed the run
queue.retry_after_secondsHow long to back off when runtime_unavailable
queue.positionAlways null; Sume does not publish queue depth
primary_output_urlThe one item to show, or null

A loop that backs off and respects the ceiling

Double the gap from 5 seconds up to one minute, as the docs recommend. A 429 or 503 on a read means the read failed, not the run, so wait and try again. The snippet prints the deadline once and lets the caller decide what to do if it passes.

import asyncio
import os
import sys

import httpx


async def main(run_id: str):
    headers = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}
    gap = 5
    async with httpx.AsyncClient(base_url="https://api.sume.com", headers=headers) as c:
        while True:
            r = await c.get(f"/v1/format-runs/{run_id}")
            if r.status_code in (429, 503):  # the read failed, not the run
                await asyncio.sleep(gap)
                continue
            r.raise_for_status()
            run = r.json()["data"]
            if run["status"] not in ("queued", "processing"):
                break
            print(run["status"], "deadline:", run.get("expires_at"))
            await asyncio.sleep(gap)
            gap = min(gap * 2, 60)
    print(run["status"], run.get("primary_output_url"), run.get("error"))


asyncio.run(main(sys.argv[1]))

Stuck in queued

If queued lasts, read queue. waiting is normal. runtime_unavailable means the run waited longer than the pickup window and nothing claimed it, and retry_after_seconds says how long to back off. The page says that if it lasts more than a few minutes you should send a support ticket with the request_id.

A client timeout does not cancel the run. Store the run id and read it later, or cancel it explicitly with POST /v1/format-runs/{run_id}/cancel. If you would rather not poll at all, pass communication.webhook_url on the create and keep this loop as a fallback.

Choosing the ceiling

Your loop has two exits: a terminal status, and the deadline. If the deadline passes while the receipt still says processing, read it once more before you give up, because Sume finalizes the run on its side and the next read shows failed with an error. That final read gives you the reason, so you can log it and tell the user what happened.

Do not hard-code 90 minutes. The deadline can be earlier than that for a quiet run, and the docs tell you not to invent a different timeout. Read the field on every poll, and compare it with the clock on your own machine only to decide how long to keep sleeping. If your server clock drifts, trust the status over the timestamp.

For a service that holds many runs, keep one loop per run id with a shared httpx client, and cap how many loops run at once so your reads stay inside your plan's read limit.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume