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.

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
- Sume jobs_wait outcome: wait_slice_expired is not a failed job
jobs_wait returns outcome terminal, wait_slice_expired or operator_stopped. Only terminal means the jobs ended; an expired slice says nothing about the jobs.
- jq one-liners for the video model catalog: ids, durations, 1080p
Two jq filters over GET /v1/videos/models: a table of ids with min and max seconds, and a filter for models that take 20 s at 1080p. Tested on a local copy.
- Kling 3 image-to-video on Sume: the first frame sets the shape
With a first frame, Sume's kling-3 builder does not send aspect_ratio. Crop the image to the shape you want before you submit. Here is the request.
- Kling 3 negative prompt and cfg_scale on Sume: fixed, no field
Sume's kling-3 sends its own negative prompt and a cfg_scale of 0.5 to the provider. There is no public field to change either, so steer with the prompt itself.
Written by Sume