Timeouts, retries, backoff and jitter
coreadvancedA **timeout** is the longest you are willing to wait before giving up on a call. A **retry** is trying again after a failure. **Exponential backoff** doubles the wait between attempts, so a struggling service gets quieter traffic instead of more. **Jitter** adds randomness to that wait, so a thousand clients do not all come back at the same instant. Retrying is only safe when the call is idempotent — running it twice has the same effect as running it once.
Think of it as
Start from the fact that every remote call has an unbounded worst case unless you bound it. Requests states it plainly: "By default, requests do not time out unless a timeout value is set explicitly. Without a timeout, your code may hang for minutes or more." A gunicorn worker stuck in that call is a worker serving nobody, and the failure spreads outward — the worker pool fills, the queue behind it fills, and a slow dependency you do not own takes your site down. So the timeout is not a nicety, it is the thing that converts *their* outage into *your* handled error. Set two numbers, not one: the connect timeout bounds establishing the TCP connection, the read timeout bounds waiting for bytes after the request is sent, and `timeout=(3.05, 27)` sets them separately. Next comes the budget, and this is where most retry code goes wrong. The user is waiting for one HTTP response, so the total time you may spend is fixed — say 10 seconds. Three attempts at a 10-second timeout is a 30-second worst case, which means the retries do not make the call more reliable, they make the request time out somewhere further up. Pick the per-attempt timeout so that attempts × timeout + waits fits inside the budget you actually have. Then decide what is worth retrying at all. A timeout, a connection refused, a 502/503/504 — those are transient and may succeed on the next attempt. A 400, a 401, a 404, a validation error — those will fail identically forever, and retrying them only spends your budget. A 429 is special: it is the server telling you the rate is too high, and it often carries `Retry-After`, which you should obey rather than compute. Backoff exists because a failing service is usually failing *because* of load, and a fixed 100 ms retry loop from every client is a denial-of-service attack you wrote yourself. Doubling — 1s, 2s, 4s, 8s — drops the pressure fast. Jitter exists because backoff alone still synchronises: every client that saw the outage at the same moment retries at the same moment, so the recovering service gets a wall of traffic, falls over again, and the herd re-synchronises. Randomising the sleep spreads the same number of attempts across the window. The last piece is the one that is not about waiting at all. A retry can duplicate work, because a timeout does not tell you whether the other side did the thing — it tells you that you did not hear back. If the call charges a card, the safe version sends an idempotency key so the second attempt returns the first attempt's result instead of charging twice.
What we're doing: Call a payment provider with a bounded budget, a retry policy that knows what is worth retrying, and a duplicate-safe key.
- 8–9
- Two separate numbers. The connect timeout bounds reaching the host; the read timeout bounds waiting for a response after the request is sent. Requests applies a single value to both, which usually means one of them is wrong.
- 13–14
- The budget is stated as a comment because it is the constraint the other numbers must satisfy. Attempts and per-attempt timeout multiply — three attempts at 30 seconds is a 90-second worst case no user is waiting for.
- 22–24
- The key makes the retry safe. Without it a read timeout leaves you unable to tell "the charge did not happen" from "the charge happened and the reply was lost", and the only safe choice would be not to retry at all.
- 30–33
- Non-retryable statuses return immediately. Retrying a 400 spends the budget on a request whose outcome cannot change.
- 35–37
- `Retry-After` is the server's own number. Obeying it beats any backoff you compute, because the server knows when the limit resets and you do not.
- 39–42
- Full jitter: sleep uniformly between zero and the exponential backoff for this attempt. The mean wait is halved and the arrivals stop lining up.
Why this works: The worst case is bounded and known, only transient failures consume attempts, a struggling provider sees decreasing and spread-out load, and a duplicate attempt cannot charge a customer twice.
Retrying without an idempotency key after a read timeout
Wrong
Better
What you see: A small, steady trickle of customers charged two or three times, always during a period when the provider was slow rather than down — and no error in your logs, because every attempt that mattered succeeded.
Why: A read timeout means you stopped waiting. It does not mean the other side stopped working: the request may have arrived, been processed and committed, with only the response lost. So the retry is a second, independent charge. An idempotency key closes the gap by moving deduplication to the side that knows — the provider stores the key with the first result and returns that same result for any later request carrying it. The key must be generated once, per logical operation, and reused across attempts; generating it inside the loop gives every attempt a new key and restores the original bug.
- Two timelines, both running from 0 to 8 seconds, each showing nine retry attempts from three clients that failed at the same moment.
- The top timeline, labelled "exponential backoff, no jitter", has three tall red bars, at 1 second, 2 seconds and 4 seconds. Each bar is three attempts high, because all three clients retry on exactly the same schedule.
- The bottom timeline, labelled "backoff plus full jitter", has nine short green bars scattered irregularly between about 0.4 and 6 seconds, one attempt high each.
- The two panels carry the same total: nine attempts. Only their arrival pattern differs.
- A note marks the top panel as the shape that knocks a recovering service over again.
What each failure class deserves
Together
The four waiting strategies, on the same failing call
Together
Remember: Every outbound call gets a timeout, because `requests` has none by default and a hung call holds a worker forever. Set connect and read separately, and size attempts × timeout to fit the request budget you actually have. Retry only transient failures — timeouts, connection errors, 502/503/504, and 429 with its `Retry-After` — never a 400 or a 404. Back off exponentially so a struggling service sees less traffic, and add jitter so every client does not come back at the same instant. And retry only what is idempotent: a read timeout does not tell you whether the work happened, so carry an idempotency key generated once and reused on every attempt.
See also: circuit breakers bulkheads and rate limits · idempotency keys and http semantics · retries backoff and scheduling · testing retries timeouts and idempotency

