503 status_busy on GET /v1/jobs/{id}/status: back off and jitter

status_busy means Sume's job status read gate is full. Reads of the same job are shared; the cap is 100 distinct in-flight reads. Poll slower, add jitter.

4 min readSume
All posts

A GET /v1/jobs/{id}/status that answers 503 with the code status_busy and the message "Job status reads are busy; retry shortly" is not a rate limit on your key. It is a protective gate inside the API process. It protects the job store when many status reads are in flight at the same moment.

How the gate works

Status reads are shared. The key for an in-flight read is the workspace, the owner, the minted thread scope and the job id. If two requests for the same job arrive while a read is still running, the second one waits on the same read instead of starting another.

The gate only refuses a new, distinct read when 100 reads are already in flight. So a single hot job never trips it. Many different jobs polled at once can. A fleet of workers that all wake on the same timer and poll hundreds of different job ids is the typical cause.

What to do about it

The status is a 503, so it is retryable: nothing about your request was wrong. The fix is on the client. Use the next_poll_after_seconds field that each status response carries, instead of a fixed one-second loop, and spread workers with random jitter so they do not all poll on the same tick. Poll a job until terminal is true, then stop; a terminal job returns null for the next poll hint.

If you hold many jobs, there is a better route than N loops. Use webhook mode with a signed webhook_url, or on hosted MCP use one jobs_wait call for up to 20 job ids. Both replace most status polling.

import os, random, time, requests

H = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"]}

def status(job_id):
    delay = 2.0
    while True:
        r = requests.get(f"https://api.sume.com/v1/jobs/{job_id}/status", headers=H, timeout=30)
        if r.status_code == 503:
            time.sleep(delay + random.random() * delay)
            delay = min(delay * 2, 30)
            continue
        r.raise_for_status()
        body = r.json()
        data = body.get("data", body)
        if data.get("terminal"):
            return data
        time.sleep(data.get("next_poll_after_seconds") or 3)

Do not treat it as a failed job

A status_busy answer says nothing about the job. The job is still queued or processing, and its spend and refund rules are unchanged. Never resubmit the create call because a status read returned 503. If you must resubmit, send the same Idempotency-Key so the original is replayed, not duplicated.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume