ealeng

Episode 32 of 56 · Resilience

Retries and jitter: when trying again makes it worse

Which failed model calls to retry and which never, what retries buy against random failures and what they don't against an outage, why your retries on top of the SDK's sent 15 calls to a dead service, and how 200 customers retrying in step kept the last one waiting 152 seconds until a random wait (jitter) brought it to 12.7.

Lab 13 · Resilience11 min read
Video transcript7:17

0:00Cold open

0:01

Our sale opened at nine. Two hundred customers wrote to our help chat in the same second.

0:07

I rebuilt that rush on a test server: two hundred pretend customers at once, and a server that takes only twenty requests a second. The rest are turned away, and my code tries again.

0:18

The last customer got an answer after two and a half minutes.

0:24

Then I changed one thing: how long each customer waits before trying again. Same two hundred customers. Same server.

0:32

The last answer came in under thirteen seconds.

0:36

Why was trying again the slow part?

0:39Intro

0:40

Hands-on AI engineering: resilience, part one.

0:44

Today: retries, trying again when a request fails. When they help, and when they make things worse.

0:51Meet Sofia

0:53

I’m Sofia, a support engineer at Lantern Outfitters, a made-up outdoor gear shop.

0:58

Every customer message goes to a language model. My code sends it over the internet, to someone else’s servers. That’s a request.

1:07

Think of each request as a phone call. Sometimes nobody can take it.

1:13

To see what my code does then, I make some calls fail on purpose. A small switch in my own code fails them before they leave my laptop. The rest go to the real model.

1:2501 · What to retry

1:26

When a call fails, it comes back with a number that says why.

1:30

Four twenty-nine means “too many requests”. It’s a busy signal. Call back in a moment, and it may work.

1:37

Five hundred, or five oh three, means the server is in trouble. That can pass too.

1:43

A timeout means nobody picked up in time. Also worth another call. But careful: the first call may still go through, so calling again can do the job twice, like placing an order twice.

1:56

Four hundred means my request itself is wrong, like dialing a wrong number. Calling again fails the same way. Never retry it.

2:0602 · Coin flips

2:08

Calling back works best when failures are random. So I made half of all calls fail, at random, like a coin flip.

2:15

With one try each, about half of a hundred customers got through, as you’d expect.

2:21

With up to five tries each, how many get through?

2:26

Ninety-six.

2:29

Each try is a fresh coin flip. Failing five times in a row is rare: one chance in thirty-two. So about three in a hundred should still fail. Four did.

2:41

The price: each customer cost two calls, on average. If those failures were real, that’s twice the load on the server.

2:5003 · An outage

2:51

Real failures aren’t always coin flips. Next, I made every call fail for six seconds, as if the model were down.

2:59

Now the line is dead. Every call back in those seconds fails too.

3:03

Customers kept writing during those seconds. With up to five tries each, how many got through?

3:11

Nine of twenty-four.

3:15

With up to eight tries, all of them did. But the slowest waited over eight seconds.

3:21

Retries get through an outage only if people wait until it’s over. To do better, you need a backup. That’s part two.

3:2904 · Retries × retries

3:30

One more trap. The OpenAI SDK, the library my code uses to place each call, already retries on its own: three tries in all.

3:40

My old code wrapped every call in five tries of its own. Each of my five tries turned into three calls inside the SDK.

3:49

Against a dead server, one customer’s message sent fifteen calls, then failed anyway.

3:56

With the SDK’s retries turned off, my five tries sent five calls, and failed much sooner. Retry in one place, not two.

4:0605 · Backoff

4:07

Back to the sale. Again, a test server on my laptop. It takes twenty calls a second, and turns the rest away.

4:15

My retries used backoff: after each failure, wait twice as long before calling again. Half a second, then one, two, four, up to eight.

4:25

The idea is to give a busy server room to breathe.

4:28

But almost all two hundred customers were turned away at the same moment. So they all waited the same half second, and called back at the same moment.

4:3906 · Waves

4:40

Green bars are calls let in. Red bars are calls turned away.

4:45

Each tall bar is a wave: everyone calling back together.

4:49

At fifteen seconds, a hundred and thirteen customers called at once. Twelve got in.

4:56

Why only twelve? The server can’t save up its quiet seconds. In that moment, it had room for about a dozen.

5:04

The rest waited eight seconds. Meanwhile, the server sat idle. Then they all called back together again.

5:11

Wave after wave. The last customer got an answer after a hundred and fifty-two seconds.

5:1907 · Jitter

5:21

The fix is called jitter. Each customer waits a random time, anywhere from zero up to that doubling wait. So each one calls back at their own moment.

5:31

Same backoff. Same server. Same two hundred customers. When does the last one get an answer?

5:39

In under thirteen seconds. The calls spread out, so the server stays busy instead of idle.

5:46

The fastest possible is ten: two hundred customers, at twenty a second. And it took fewer calls.

5:53

Waves depend on timing, so I ran each setup four times. Jitter won every time, by more than ten times.

6:0208 · The server says when

6:04

Some servers say when to call back, like a recorded message: “try again in one second.” If every customer obeys, do the waves go away?

6:15

No. Everyone waited at least that one second, then backed off exactly as before. Same wait for everyone, so they still called back together. The last answer: a hundred and sixty seconds.

6:28

Now each customer waits the server’s one second, plus a random extra. The last answer: seventeen seconds.

6:3709 · What I do now

6:38

So here’s what I do now. I retry only what can pass: busy, server trouble, timeouts. Never a wrong number.

6:46

I retry in one place: the SDK, or my code. Not both.

6:51

Every wait gets jitter, even when the server says how long.

6:56

And I don’t count on retries to get through an outage.

7:00

Next time: the model goes down for twenty-five seconds. A backup model, and a circuit breaker.Sofia: Add a little randomness.

7:07Summary

7:08

Retries fix blips. Outages need a plan B.

7:12

Retry in one place, and never all at once.Sofia: Thanks for watching!

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.

Try it · When retries helpLab 13 · script 01 · failures injected under the real SDK

Share of calls that fail, at random

1 try (no retries)57 of 100

expected 1 − 0.51 = 50.0% · 1.00 calls per customer · slow end 1.12 s

3 tries (the SDK's default)87 of 100

expected 1 − 0.53 = 87.5% · 1.82 calls per customer · slow end 2.13 s

5 tries (ours, backoff + jitter)96 of 100

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.

Try it · Retries on top of retriesLab 13 · script 01, Exercise 1 · a service that returns 503 every time

Your code's tries

The SDK's tries

A gateway's tries

Calls one message sends
15
Time until it gives up
8.4 s

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:

Try it · The rush: 200 customers at onceLab 13 · script 01, Exercise 4 · a test server that lets in 20 a second

wait 0.5, 1, 2, 4, then 8 s, the same for everyone

Last answer
152.0 s
Half answered by
23.7 s
Calls the server saw
1,643
last answer 152.0 s0 s30 s60 s90 s120 s150 s
let in turned away (429) the server's 20 a secondOne bar per second. Bars over 120 calls are cut off (black tick).

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

  1. Retry only what can pass: 429, 5xx, timeouts, dropped connections. Raise a 400, 401 or 403 at once.
  2. Treat a timed-out write as "unknown": retry it only with an idempotency key.
  3. Retry in one layer. Count the tries in your code, the SDK and any gateway; turn all but one off (max_retries=0).
  4. Set a time limit on every call. The SDK's default is 600 s per try.
  5. Back off exponentially, with a cap, and jitter every wait, including waits the server asked for.
  6. Obey Retry-After as a floor, not as the whole wait.
  7. 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.

Try it · Practice in PythonRuns in your browser · ⌘/Ctrl + Enter to run
retry_practice.py92 lines

Quiz

Try it · Check your understanding0 of 6 answered
  1. 1.A model call fails with a 400 because the prompt is longer than the context window. What should your retry code do?

  2. 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. 3.Every call fails for 6 seconds. Why did 5 tries get only 9 of the 24 calls sent during that time through?

  4. 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. 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. 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.

Learning something useful?

New labs, articles, and videos on evaluating and testing AI systems come out regularly. Follow along so you don't miss the next one.