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.

4 min readSume
All posts

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.

Sume 429 codes and the right reaction (Sume docs, read 2026-10-07)
CodeMeaningReaction
rate_limitedRequest volume over a window; details.scope says read or writeSleep retry-after, then retry
queue_fullNo accepted generation capacity leftWait 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

All Developers posts

Written by Sume