Key takeaways
- ✓Retry only what can go away on its own: 429 (rate limited), 5xx (server trouble), timeouts and dropped connections. Never a 400 (the request itself is wrong, like a prompt over the context window), 401 or 403: they fail the same way every time. A timed-out call may still have gone through, so retrying a write can do it twice.
- ✓Against random failures, retries work as coin-flip math predicts: n tries all fail with chance pⁿ. At 50% failures, 57 of 100 got through with 1 try, 87 with 3, 96 with 5, each inside its interval of 1 − 0.5ⁿ. The price is load (2.01 calls per customer) and a longer slow end.
- ✓An outage isn't coin flips: every call in those seconds fails together. With every call failing for 6 s, 4 of the 24 calls sent during it got through with the SDK's 3 tries and 9 with 5; 8 tries over up to 20 s got all 24, with the slowest customer waiting 8.15 s. Retries outlast an outage only by making people wait. That needs a backup (chapter 33).
- ✓The OpenAI SDK already retries twice (3 tries in all). Five tries of your own around it sent 15 calls to a service returning 503, and took 8.4 s to fail; with the SDK's retries off, 5 calls and 1.8 s. Layers multiply: retry in one place.
- ✓200 customers turned away at once, all backing off by the same 0.5, 1, 2, 4, 8 s, call back together in waves. One wave at 15 s: 113 calls, 12 let in by a server that can't save up its quiet seconds, then 7.8 s of silence. The last answer came at 152 s; with full jitter (a random wait from 0 up to the backoff), 12.7 s, with fewer calls. Over 4 runs: 144–192 s without, 12–14 s with.
- ✓Retry-After alone doesn't break the waves: everyone waits at least the server's 1 s, then backs off in step (160.5 s). The server's wait plus a random extra: 17.1 s. Put jitter on every wait.
A retry is calling again after a model call fails. Done right, it turns a passing blip into an answer. Done wrong, it multiplies load on a service that's already down, keeps customers waiting through an outage, or sends everyone back at the same instant so a rate limit turns into minutes of waiting. This chapter covers which failures to retry, what retries buy and what they don't, retries stacked on retries, and the waves 200 customers made, until jitter broke them up.
The source is Lab 13 (Resilience), script 01_retries.py and resilience.py, recorded in October 2026 with openai 3.35.1 on one laptop. The failures are injected on purpose: a small switch in the lab's own code fails chosen calls before they leave the laptop, and the rest go to the real model (gpt-5.4-nano). The rush of 200 customers runs against a local test server, not the real API. Its numbers are from the video's rerun of that exercise; the lab's own run is quoted alongside. Lab 13's later scripts are the next chapters: fallbacks and the circuit breaker (33), crash and resume (34), and checked outputs, broken search and deadlines (35). Its tricky-inputs suite isn't in this series.
What to retry
A failed call comes back with a number that says why. Retry only what can go away on its own:
| Failure | What you see | Retry it? |
|---|---|---|
| Rate limit | 429, often with Retry-After |
yes, after waiting at least that long |
| Server error, overload | 500, 502, 503, 504 |
yes, with backoff |
| Hang | nothing until your time limit: APITimeoutError |
yes, but the first call may still finish |
| Dropped connection | APIConnectionError, or a stream cut off |
yes |
| Bad request | 400: bad parameters, a prompt over the context window |
never |
| Bad key, no permission | 401, 403 |
never |
A 400 fails the same way every time, so retrying only wastes time and money. A timeout needs care: "timed out" means "unknown". The server may still do the work, so retrying a write (a refund, an order) can do it twice. That's what idempotency keys are for (chapter 34).
The lab's is_retryable() is that table in code:
RETRYABLE_STATUS = {408, 409, 429, 500, 502, 503, 504}
def is_retryable(exc):
if isinstance(exc, (openai.APIConnectionError, httpx.TransportError, TimeoutError)):
return True # includes openai.APITimeoutError
return getattr(exc, "status_code", None) in RETRYABLE_STATUS
When retries work: coin flips
Retries work best when failures are random and independent. If each call fails with chance p, n tries all fail with chance pⁿ, so at least one gets through with chance 1 − pⁿ. The lab failed calls at random with a 503, 100 customers per cell:
| Calls failing | 1 try | 3 tries (SDK default) | 5 tries (ours) | Calls per customer, ours | Slow end (p95), ours |
|---|---|---|---|---|---|
| 10% | 89 | 100 | 100 | 1.10 | 1.09 s |
| 30% | 69 | 96 | 99 | 1.37 | 1.56 s |
| 50% | 57 | 87 | 96 | 2.01 | 2.26 s |
At 50%, coin-flip math predicts 50%, 87.5% and 96.9%; every measured count is inside its 95% interval. Five failures in a row happen 1 time in 32, so about 3 in 100 should still fail; 4 did.
The price: at 50% failures, each customer cost 2.01 calls. These failures were injected on the laptop, but if they were real overload, that's twice the load on a server that's already struggling. The typical time barely moved (0.63–0.80 s); the waits land in the slow end.
When they don't: an outage
Real failures aren't always coin flips. The lab made every call fail for 6 seconds, as if the model were down, while customers kept writing: 80 calls, one every 0.25 s, 24 of them during the outage.
| Policy | Got through, of the 24 | Slow end | Slowest |
|---|---|---|---|
| SDK default, 3 tries | 4 | 1.95 s | 2.11 s |
| Ours, 5 tries | 9 | 3.93 s | 4.17 s |
| Ours, 8 tries, 20 s deadline | 24 | 6.95 s | 8.15 s |
Every retry during those seconds fails too. Only the setup that kept trying until the outage was over got everyone through, and its slowest customer waited over 8 seconds. Retries outlast an outage only by making people wait as long as it lasts. For an outage, you need a backup: chapter 33.
Share of calls that fail, at random
expected 1 − 0.51 = 50.0% · 1.00 calls per customer · slow end 1.12 s
expected 1 − 0.53 = 87.5% · 1.82 calls per customer · slow end 2.13 s
expected 1 − 0.55 = 96.9% · 2.01 calls per customer · slow end 2.26 s
Each try is a fresh coin flip, so n tries all fail with chance pn. Every measured count is inside its 95% interval of that prediction (the yellow mark). The price is load: at 50% failures, 5 tries sent 2.01 calls per customer, and the waits sit in the slow end, not the typical time.
Retries on top of retries
The OpenAI SDK, the library that places each call, already retries on its own: 2 retries, 3 tries in all, with backoff and jitter. It honors Retry-After and never retries a 400. The lab measured it against a service that always fails (none of these reached the API):
| Server does | Tries | Waits between them | Error after |
|---|---|---|---|
| 503 every time | 3 | 0.5 s, 0.8 s | 1.3 s |
429 with Retry-After: 2 |
3 | 2.0 s, 2.0 s | 4.0 s |
| 400 bad request | 1 | none | 0 s |
| hangs (time limit set to 2 s) | 3 | 2.5 s, 2.9 s | 7.4 s |
The video skips one result from the lab: the SDK's default read timeout is 600 s. A server that accepts the connection and never answers would hold a call for 10 minutes per try, half an hour in all (computed, not waited out). Set your own time limit on every call.
Now wrap that in your own loop. Every one of your tries becomes 3 calls inside the SDK:
| Your tries | SDK tries | Calls sent | Time until it gives up |
|---|---|---|---|
| 1 | 3 | 3 | 1.3 s |
| 3 | 3 | 9 | 4.5 s |
| 5 | 3 | 15 | 8.4 s |
| 5 | 1 (retries off) | 5 | 1.8 s |
client = OpenAI(max_retries=0, timeout=10) # the SDK doesn't retry; our retry() does
Add a gateway that also retries 3 times, and one user message sends 3 × 3 × 3 = 27 calls with default-ish settings. Retry in one place, usually the layer closest to the user, which knows how long they can wait, and turn the others off.
Your code's tries
The SDK's tries
A gateway's tries
2 layers retry, so the tries multiply: 5 × 3 = 15 calls to a service that's down, and the message still fails.
Measured (openai 3.35.1, no gateway): 5 × 3 = 15 calls and 8.4 s to give up; with the SDK's retries off, 5 calls and 1.8 s. The gateway row is the arithmetic, not a measurement. The SDK retries twice by default (3 tries in all).
The rush: backoff without jitter
Exponential backoff waits longer after each failure, to give a busy server room to recover: here 0.5 s, 1, 2, 4, then 8 s at most. The lab's test:
- 200 customers send one call each at the same moment;
- a local test server lets in 20 calls a second, with at most 5 spare saved up, and turns the rest away with a 429;
- each customer retries up to 30 times with that backoff. The fastest possible finish is 10 s (200 at 20 a second).
Almost everyone is turned away at once, so almost everyone waits the same 0.5 s and calls back at the same moment. And again, and again: the calls arrive in waves.
One wave, from the server's log: at 15 s, 113 customers called within 0.37 s, and 12 got in. Why only 12? The server can't save up its quiet seconds: it holds at most 5 spare, and adds 20 a second, so in 0.37 s it had room for about 5 + 20 × 0.37 ≈ 12. The other 101 waited 8 s; for 7.8 s nobody called, with the server sitting idle; then they all called back together. Wave after wave, the last customer got an answer after 152 seconds, after 1,643 calls.
Jitter: a random wait
Jitter makes each wait random. With full jitter, each customer waits anywhere from 0 up to the backoff:
def delay(n, base=0.5, cap=8.0):
return random.uniform(0, min(cap, base * 2 ** n)) # full jitter
Same backoff, same server, same 200 customers: the last answer came after 12.7 s, with fewer calls (987). The calls spread out, so the server stays busy instead of idle.
| Retry timing | Calls the server saw | Half answered by | Last answer |
|---|---|---|---|
| Backoff, no jitter | 1,643 | 23.7 s | 152.0 s |
| Backoff + full jitter | 987 | 4.9 s | 12.7 s |
Retry-After: 1, no jitter |
2,015 | 40.2 s | 160.5 s |
Retry-After: 1 + jitter |
801 | 5.4 s | 17.1 s |
Waves depend on which customers win each race, which depends on thread timing, so the lab's run and the video's three reruns differ: last answer 192.1, 152.0, 144.1 and 144.0 s without jitter, 13.9, 12.7, 13.1 and 12.4 s with. Jitter won every pairing by more than 10×. The lab's own run (192 s, 2,640 calls vs 13.9 s, 984) is the README's table.
When the server says when
Some servers say when to call back: a Retry-After header, "try again in 1 second". Obey it: wait at least that long. But obeying it doesn't break the waves. Every customer waits the longer of the server's 1 s and the backoff, so the waits are still the same for everyone, and they still return together: 160.5 s. The server's wait plus a random extra of up to 1 s, with the jittered backoff: 17.1 s.
wait = max(delay(n), retry_after) # Retry-After is a floor
wait += random.uniform(0, 1) # and still add jitter
Switch between the four setups. Each bar is one second; green calls got in, red were turned away:
wait 0.5, 1, 2, 4, then 8 s, the same for everyone
Customers turned away together wait the same time and call back together: each tall bar is a wave. At 15 s, 113 called within 0.37 s and 12 got in, because the server can't save up quiet seconds (it holds at most 5 spare). Then 7.8 s of silence while the server sat idle, and the next wave.
This is the video's rerun. Without jitter the result depends on thread timing, so it varies: last answer over 4 runs 192.1, 152.0, 144.1, 144.0 s without jitter, 13.9, 12.7, 13.1, 12.4 s with it. The lab's own run of this setup: 192.1 s, 2,640 calls. Jitter won every pairing by more than 10×.
Caveats
- Injected failures. The coin flips, the outage and the stacked retries fail calls on the laptop before they leave it, so the "load on the server" they'd cause is an inference, not a measurement.
- A test server, not the real API. The rush runs 200 simulated customers (threads) against a local server set to 20 a second with 5 spare. A real provider's limits, and its other customers, are different.
- One laptop, a few runs. The waves vary with thread timing: 144–192 s across 4 runs. The jitter results were steady (12–14 s).
- Full jitter isn't the only kind. "Equal" jitter (half the backoff, plus a random half) guarantees a minimum wait, which is kinder to a service that needs time to recover. Both beat no jitter by far; the lab measured only full jitter.
- Retries add load. If a service fails because it's overloaded, retrying at 2 calls per customer keeps it down. A circuit breaker stops adding load; that's chapter 33.
The checklist
- Retry only what can pass: 429, 5xx, timeouts, dropped connections. Raise a 400, 401 or 403 at once.
- Treat a timed-out write as "unknown": retry it only with an idempotency key.
- Retry in one layer. Count the tries in your code, the SDK and any gateway; turn all but one off (
max_retries=0). - Set a time limit on every call. The SDK's default is 600 s per try.
- Back off exponentially, with a cap, and jitter every wait, including waits the server asked for.
- Obey Retry-After as a floor, not as the whole wait.
- Don't count on retries for an outage. Bound them with a deadline, and plan a fallback.
Practice: retry, and count
Write the lab's pieces from scratch: which failures to retry, the backoff schedule with full and equal jitter, the coin-flip prediction and its Wilson interval, how many calls stacked layers send to a dead service, a retry loop that honors Retry-After, and the test server's token bucket. The tests use the recorded run: 57 / 87 / 96 of 100 inside their intervals, 15 calls for 5 × 3 tries, and the 113-call wave at 15 s, replayed through the bucket, letting exactly 12 in.
Quiz
1.A model call fails with a 400 because the prompt is longer than the context window. What should your retry code do?
2.Half of all calls fail at random. With up to 5 tries each, about how many of 100 customers should get through, and what does it cost?
3.Every call fails for 6 seconds. Why did 5 tries get only 9 of the 24 calls sent during that time through?
4.Your code tries 5 times, and the OpenAI SDK under it tries 3 times by default. How many calls does one message send to a service that's down?
5.200 customers are turned away at the same moment and all back off by 0.5, 1, 2, 4, then 8 s. Why did the last one wait 152 s for a server that can do all 200 in 10?
6.The server sends Retry-After: 1, and every customer obeys it. Why are there still waves?
Check your understanding
Try each question before opening its answer.
Which failures should you retry, and which never?
Retry what can go away on its own: 429 rate limits (after at least the Retry-After), 500/502/503/504 server errors, timeouts and dropped connections. Never retry a 400 (the request is wrong, for example a prompt over the context window), a 401 or a 403: they fail the same way every time.
A call timed out. Is it safe to retry?
For a read, yes. For a write, only with an idempotency key: a timeout means "unknown", and the server may still have done the work, so a retry can place the order or the refund twice.
Why did 5 tries get 96 of 100 through at 50% random failures, but only 9 of 24 through a 6-second outage?
Random failures are independent coin flips: 5 in a row fail 1 time in 32. In an outage, every try within those seconds fails together, so only customers whose retries reached past the outage got through. 8 tries over up to 20 s got all 24, with the slowest waiting 8.15 s.
How many calls does one message send to a dead service with your 5 tries around the SDK's defaults?
15: the SDK tries 3 times (2 retries) inside each of your 5. Measured: 15 calls and 8.4 s to give up; with the SDK's retries off, 5 calls and 1.8 s. Retry in one place.
Why did the rush without jitter take 152 s, when the server could finish all 200 in 10?
Everyone turned away together waited the same time and called back together, in waves. The server can't save up quiet seconds (5 spare at most, 20 a second), so a wave of 113 calls over 0.37 s let in 12; the rest waited 8 s while the server sat idle. Full jitter spread the calls out: 12.7 s, with fewer calls (987 vs 1,643).
The server sends Retry-After: 1. Is obeying it enough?
No. Obey it as a floor, but everyone waiting at least the same 1 s, then backing off in step, still returns together: 160.5 s. The server's wait plus a random extra: 17.1 s. Put jitter on every wait.
Next up: Chapter 33: the main model goes down for 25 seconds. A backup model keeps answering, and a circuit breaker stops calling a dead one, but only as fast as failures arrive.