Poll a Sume job in Python with a deadline and next_poll_after_seconds

A Python wait loop for a Sume job: stop on terminal, sleep at least next_poll_after_seconds, and give up at a monotonic deadline. A timeout is not a failure.

4 min readSume
All posts

A polling loop with no end is a bug that waits for the right bad day. Generation jobs on the Sume API can sit in a queue behind your workspace's concurrency limit, so the time a job takes is the sum of waiting and running, and you do not control the first part. Your own code needs a ceiling that is separate from the API's.

The status route makes the loop short. GET /v1/jobs/{id}/status returns terminal, a sume_status, a result_url and a next_poll_after_seconds hint. The hint is a minimum, not a schedule: asking sooner just spends read budget on an answer that cannot have changed.

Three rules for the loop

Polling rules for the job status route, from the Sume docs (read 2026-10-03)
RuleReason
Stop when terminal is truecompleted, failed and canceled are all terminal, so do not test for completed alone
Sleep at least next_poll_after_secondsThe API names the earliest useful next read
Check a monotonic deadline before each sleepWall-clock changes cannot stretch or cut your ceiling

The loop

The function uses time.monotonic() for the deadline and refuses to start a sleep that would end after it. That means it raises before the deadline instead of overshooting by one pause. The sample stops after one success, and a second call with a short deadline would raise. It returns the status data for every terminal state, so the caller reads sume_status and decides what failure means.

import json, os, time, urllib.request

HEAD = {"x-api-key": os.environ["SUME_API_KEY"]}


def status(job_id: str) -> dict:
    url = f"https://api.sume.com/v1/jobs/{job_id}/status"
    with urllib.request.urlopen(urllib.request.Request(url, headers=HEAD), timeout=30) as r:
        return json.load(r)["data"]


def wait(job_id: str, deadline_s: float = 900.0, floor_s: float = 2.0) -> dict:
    end = time.monotonic() + deadline_s
    while True:
        s = status(job_id)
        if s["terminal"]:
            return s
        pause = max(floor_s, s.get("next_poll_after_seconds") or 0)
        if time.monotonic() + pause > end:
            raise TimeoutError(f"{job_id} still {s['sume_status']} after {deadline_s:g}s")
        time.sleep(pause)


s = wait("job_1", deadline_s=30)
print(s["sume_status"], s["result_url"])

What a timeout means

  • TimeoutError says your loop gave up, not that the job failed. The job keeps running and may still bill, so store the job id and check it later instead of resubmitting.
  • Resubmitting after a timeout creates a second paid job unless you send the same Idempotency-Key. Waiting longer is usually the cheaper answer.
  • A non-completed terminal state is a separate branch. Read GET /v1/jobs/{id} for the error object when sume_status is failed.
  • Count reads. Polling one job every 2 seconds is 30 reads a minute, and reads default to 40 times the write budget, so a handful of jobs is fine and a large pool should share a single list call.
  • Choose the deadline from the job type. A short clip and a long render should not share one number, and a webhook is a better fit for the long ones.

The status fields and the polling advice are in the jobs guide.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume