Polling a 5-minute video job: 150 GETs at 2 s, or 10 at 30 s

A five-minute Sume video job costs 150 status reads at a 2 s interval and 10 at 30 s. Poll /v1/videos with a loop that handles every status.

5 min readSume
All posts

For a video job that runs five minutes, polling every 2 seconds costs 150 status reads, and polling every 30 seconds costs 10. The Sume /v1/videos guide uses a 30-second interval and says video generation usually takes from 30 seconds to several minutes. So use the long interval, honor next_poll_after_seconds when a status response carries it, and stop on a terminal status.

The read counts

The arithmetic is job seconds divided by the interval. Multiply by the number of jobs to see the load on the read budget.

Status reads for a 300-second job, as of 2026-10-08
Poll intervalReads per jobReads for 100 jobs
2 s15015,000
10 s303,000
30 s101,000
60 s5500

Read the status vocabulary carefully

Sume documents status reads as poll backpressure, separate from generation concurrency, and says status and list endpoints can be rate limited. A spelling detail is easy to miss. The /v1/videos poll response uses pending, in_progress, completed, failed and cancelled with two Ls. The job endpoints under /v1/jobs use queued, processing, completed, failed and canceled with one L. A loop that checks for only one spelling never terminates on the other surface.

Status words by surface, as of 2026-10-08
SurfaceNon-terminalTerminal
GET /v1/videos/{id}pending, in_progresscompleted, failed, cancelled
GET /v1/jobs/{id}/statusqueued, processingcompleted, failed, canceled

A loop that handles both

The loop below sleeps 30 seconds by default, treats either spelling of canceled as terminal, and downloads unsigned_urls[0] on success. A client-side deadline stops the wait without canceling the job, so store the job id.

import asyncio, os
import httpx

DONE = {"completed", "failed", "cancelled", "canceled"}

async def wait_video(poll_url: str, every: float = 30, limit: float = 1200):
    key = os.environ.get("SUME_API_KEY", "")
    if not key:
        raise SystemExit("set SUME_API_KEY")
    headers = {"Authorization": f"Bearer {key}"}
    waited = 0.0
    async with httpx.AsyncClient(headers=headers, timeout=30) as c:
        while waited < limit:
            r = await c.get(poll_url)
            r.raise_for_status()
            s = r.json()
            if s["status"] in DONE:
                return s
            await asyncio.sleep(every)
            waited += every
    raise TimeoutError("still running; the job keeps billing, keep its id")

async def main():
    s = await wait_video("https://api.sume.com/v1/videos/JOB_ID")
    print(s["status"], (s.get("unsigned_urls") or [None])[0])

asyncio.run(main())

Prefer a webhook for long work

If you run hundreds of jobs, send callback_url (HTTPS only) and keep a slow poll as a backup. The webhook carries the standard Sume job envelope, and a missed one can be recovered from the status URL.

Use the hint when the API gives one

Status responses on the job endpoints can carry next_poll_after_seconds. When it is present, obey it. When it is absent, back off. A fixed 30-second sleep is a fine default for video, and you can start shorter for image jobs, which often finish within the 30-second wait of a synchronous call. Do not poll many jobs in a tight loop, because status endpoints are rate limited and a 429 on a read is poll backpressure.

A deadline in your client does not cancel the job. The job keeps running and keeps billing, so store the id, and ask again later or cancel it explicitly if it has not started.

If you start a wave of jobs, poll them from one scheduler with a shared sleep, not from one task per job with its own tight loop. Or send callback_url on the submit and poll only the jobs that have no event after a safe delay. On the hosted MCP side, one jobs_wait call can take up to 20 job ids, but that is a different surface from the REST routes in this post.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume