next_poll_after_seconds is null on a finished job: fix the poll loop

Sume sets next_poll_after_seconds to null once a job is terminal, so sleep(null) crashes naive loops. Stop on terminal, then read result_ready.

5 min readSume
All posts

On a terminal job, Sume returns next_poll_after_seconds: null, so a loop that always does sleep(status["next_poll_after_seconds"]) throws on the last iteration. Check terminal first, and only sleep when the field is a number.

The same status payload sets result_ready to true only for completed. A failed or canceled job is terminal with result_ready: false, and fetching /result for it answers 409 job_not_completed.

What do the status fields look like at each stage?

In the API's job-status metadata, a non-terminal job reports next_poll_after_seconds (default 2) and recommended_poll_interval_seconds (2), and a terminal job reports null for both. terminal is true for completed, failed, and canceled.

next_action changes with the state too: poll_status while running, fetch_result when completed, inspect_events when failed or canceled.

Poll metadata by job status (API code read 2026-10-02)
Statusterminalresult_readynext_poll_after_secondsnext_action
queued, processingfalsefalsenumber (2 by default)poll_status
completedtruetruenullfetch_result
failed, canceledtruefalsenullinspect_events

Which field should the loop stop on?

Stop on terminal, not on result_ready. A loop that waits for result_ready runs forever on a failed job, because it never becomes true. The jobs docs recommend polling on the booleans or on sume_status.

After the loop, branch: fetch the result only when result_ready is true, otherwise read the job record for its error.

A poll loop that handles null

Honor the server's hint as a floor when it is present, and fall back to your own backoff otherwise. Put your own deadline outside the loop: a client-side timeout does not cancel the job, which keeps running and billing.

import asyncio, os
import httpx

BASE = "https://api.sume.com"
H = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}

async def wait(job_id: str, deadline_s: float = 1200) -> dict:
    async with httpx.AsyncClient(headers=H, timeout=30) as c:
        delay, waited = 2.0, 0.0
        while waited < deadline_s:
            s = (await c.get(f"{BASE}/v1/jobs/{job_id}/status")).json()
            d = s.get("data", s)
            if d.get("terminal"):
                return d
            hint = d.get("next_poll_after_seconds")
            delay = max(delay, hint) if hint else min(delay * 1.5, 15)
            await asyncio.sleep(delay)
            waited += delay
        raise TimeoutError(job_id)

async def main():
    d = await wait("job_123")
    print(d.get("result_ready"))

asyncio.run(main())

Is the response wrapped in data?

The submit examples in the API's OpenAPI document wrap the envelope in data. The sample above reads either shape defensively; check the live schema at https://api.sume.com/reference/json for the exact response of the route you call.

Quick checklist

The points above reduce to a short list you can paste into a runbook.

  • Check terminal before you read any sleep hint.
  • Only sleep when next_poll_after_seconds is a number; otherwise use your own backoff.
  • Fetch /result only when result_ready is true.
  • For failed or canceled jobs, read the job record or events instead of /result.
  • Set a client deadline outside the loop, and remember it does not cancel the job.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume