Sume 429: retry-after header first, then the body field, then backoff

How long to wait after a Sume 429? Use the retry-after header, then error.retry_after_seconds, then jittered backoff. A Python stdlib function with checks.

4 min readSume
All posts

After a Sume 429, wait for the retry-after response header first. If it is missing, use error.retry_after_seconds from the JSON body when it holds a number. If neither is usable, fall back to exponential backoff with jitter and a cap. The authentication docs define retry-after as the seconds to wait, sent on a 429, and the errors page describes retry_after_seconds as how long to wait before repeating a request that can succeed.

Reading both sources matters because they are not always both present. The rate limiter documents the header. Other retryable errors, such as a run that has not finished, document the hint in the error envelope, and that field can be present with a null value. A client that reads only one of them either hammers the API or waits for no reason.

The order and why

The header is the most direct signal because it comes from the rate limiter itself, so it wins. The body value is the next best, since it is a server hint tied to that specific error. Backoff is the last resort and should be jittered so that many workers do not retry in lockstep. Cap whatever you pick, because a hint in the minutes is not a reason to block a worker thread.

Where the wait hint comes from (read 2026-10-08)
SourceWhat it carriesUse when
retry-after headerSeconds to wait, sent on 429Present: always use first
error.retry_after_secondsRecommended wait before repeatingHeader missing and value is a number
error.retryableWhether the same request can succeed laterCheck before any retry
ratelimit-reset headerRate-limit window resetPacing, not the 429 wait
Exponential backoff with jitterYour own scheduleNeither hint is usable

Steps

  • Check error.retryable first. A 402 or a validation error should not be retried at all.
  • Call the function below with the response headers, the parsed body and the attempt count.
  • Sleep for the returned time, then repeat the identical request with the same Idempotency-Key on a submit.
  • Stop after a fixed number of attempts and surface the error with its request_id to your logs.

Function and checks

The cases at the bottom cover a header in mixed case, a body hint with a useless reset header, an HTTP date that cannot be parsed as a number, and an empty response. It uses only the standard library, so it runs on current Python 3 releases; we ran it on 3.14.7, not on the 3.15 release candidate.

import random


def retry_delay(headers: dict, body: dict, attempt: int, cap: float = 60.0) -> float:
    """Seconds to wait before repeating a 429/503. Header first, then body, then backoff."""
    lowered = {k.lower(): v for k, v in headers.items()}
    for raw in (lowered.get("retry-after"), (body.get("error") or {}).get("retry_after_seconds")):
        try:
            if raw is not None and float(raw) >= 0:
                return min(float(raw), cap)
        except (TypeError, ValueError):
            continue  # an HTTP-date or junk value: fall through to the next source
    return min(cap, 2.0 ** attempt) * random.uniform(0.5, 1.0)


cases = [
    ({"Retry-After": "7"}, {}, 1),
    ({"ratelimit-reset": "30"}, {"error": {"retry_after_seconds": 12}}, 1),
    ({"retry-after": "Wed, 21 Oct 2026 07:28:00 GMT"}, {"error": {"retry_after_seconds": None}}, 3),
    ({}, {}, 10),
]
for headers, body, attempt in cases:
    print(round(retry_delay(headers, body, attempt), 1))

What Sume does not do

Sume does not promise that a retry will succeed after the hinted wait; if a queue is still full, you will get another 429. It also does not send an HTTP date in retry-after in anything we read, which is why the function treats an unparsable value as missing instead of trying to parse dates.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume