Python urllib: honor retry-after on a Sume 429, back off without it
A stdlib retry for GET calls that sleeps for retry-after when the 429 carries it and for a capped exponential delay when it does not, with jitter.

On a 429, sleep for retry-after seconds when the header is present, and otherwise for an exponential delay capped at 30 seconds, plus a little jitter. Sume's error docs say exactly that: do a backoff, use retry-after if it is there, and do not retry an unsafe submit without an Idempotency-Key.
Two different 429s
A 429 on Sume is either rate_limited or queue_full. They need different handling, and a delay loop fits only the first. A queue_full answer means the workspace has no accepted capacity left until a job finishes or is canceled.
| Code | Meaning | Reaction |
|---|---|---|
| rate_limited | Request volume over a window; details.scope says read or write | Sleep retry-after, then retry |
| queue_full | No accepted generation capacity left | Wait for jobs to finish or cancel queued ones, then retry with the same key |
The helper
It uses only the standard library, so it runs as is. The script's last line calls a path on your own SUME_BASE; point it at any GET route. It retries only 429 and raises everything else at once. Jitter spreads concurrent clients so they do not wake at the same instant.
I did not use ratelimit-reset as a fallback because the docs do not state its unit.
import os
import random
import time
import urllib.error
import urllib.request
BASE = os.environ.get("SUME_BASE", "https://api.sume.com")
def get_with_backoff(path, tries=5):
req = urllib.request.Request(BASE + path, headers={"Authorization": "Bearer " + os.environ["SUME_API_KEY"]})
for attempt in range(tries):
try:
with urllib.request.urlopen(req, timeout=10) as res:
return res.read()
except urllib.error.HTTPError as err:
if err.code != 429 or attempt == tries - 1:
raise
hint = err.headers.get("retry-after")
delay = float(hint) if hint and hint.replace(".", "", 1).isdigit() else min(2 ** attempt, 30)
time.sleep(delay + random.uniform(0, 0.25 * delay))
print(get_with_backoff("/v1/jobs?limit=1"))Limits
This is for reads. A paid submit should also carry an Idempotency-Key on every attempt, since a 429 on a write proves nothing about whether an earlier attempt landed. Sume's budgets are per key and split by direction: the read budget is 40 times the write budget, so a poll loop should not starve the submits that spawned it. If you see 429 on reads anyway, slow the poll interval to the next_poll_after_seconds the status call gives you.
Sources
Related posts
More in Developers
- Python urllib: POST /v1/images, 200 or 202, after Imagen 4 Fast
imagen-4.0-fast-generate-001 ended Aug 17. A stdlib Python call to Sume's image route that reads the status code, then polls the job when the answer is 202.
- queue_full 429 on a Sume submit: the reservation is released
A 429 queue_full releases or refunds the failed admission's reservation. Check refunded_usd_micros in /v1/usage, then retry with the same Idempotency-Key.
- Can I submit 100 AI video jobs at once? Queue limits by plan
Accepted capacity is slots plus queue: 6 on Free, 24 on Pro, 48 on Startup, 120 on Scale. Submit 100 at once and 94, 76, 52 or 0 get 429 queue_full.
- Read the motion clip length with video inspect before Kling duration
Kling motion control on Sume reserves money from the duration_seconds you declare. Probe the reference clip with video inspect first, so the number is measured.
Written by Sume