ealeng

Episode 33 of 56 · Resilience

Timeouts, circuit breakers and fallbacks

What to do when the main model goes down for longer than retries can wait: a fallback chain (main model → cheaper model → a safe reply), what it costs in quality, a circuit breaker that stops calling the broken model, and why that breaker tripped with only 7 seconds of a 25-second hang left, and what changed when the main model was made to fail fast.

Lab 13 · Resilience10 min read
Video transcript8:17

0:00Cold open

0:01

An AI model sorts our support emails. One afternoon, I made it go silent. No errors. It just stopped answering.

0:09

I had a backup model ready. And a safety switch, called a circuit breaker, to stop calling the broken one.

0:16

Marcus still waited almost eighteen seconds for an answer.

0:20

And the switch only flipped with seven seconds of the outage left.

0:25

Why so late?

0:28Intro

0:29

Hands-on AI engineering: resilience, part two.

0:33

Today: what to do when the model goes down.

0:37Meet Sofia

0:38

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

0:44

Every email gets sorted by an AI model, G P T five point four mini. It fills in four boxes: what the customer wants, the order number, the product, and how urgent it is.

0:56

Here’s one, from Marcus. He wants to return a sleeping pad.

1:00

Think of it as a phone call. I call an expert, read out the email, and the expert answers with the four boxes.

1:08

In part one, when a call failed, I simply called again. That’s a retry. It fixes a short blip, but not a long outage.

1:17The test

1:18

Today, a long outage, on purpose. I send a hundred and twenty test emails, two every second. Another AI wrote them, to match answers I chose first.

1:29

Each row is one email, from sent to answered. Blue means the expert, mini, answered it.

1:35

For twenty-five seconds in the middle, I break the expert. Everything else is a real answer, from the real A P I.

1:4401 · A busy tone

1:45

A broken model can fail in two ways. The first: every call gets an error, at once. Like a busy tone: you know straight away.

1:54

Red is an error. With only the expert, forty-eight emails got one. That’s almost every email sent during the outage.

2:03

Marcus’s email was one of them. His error came back in about a second.

2:1002 · A backup

2:11

The fix: when the expert’s line is busy, phone a junior colleague instead. Here, a cheaper model, gpt-5.4-nano.

2:20

If the junior fails too, send a safe reply: a person will get back to you. That chain is called a fallback.

2:28

All forty-eight became answers. Amber is the junior. Not one error.

2:33

Marcus got his answer in a little over two seconds.

2:3803 · Is the junior as good?

2:40

But is the junior as good? Look at Marcus’s email again. He writes URGENT, in capitals.

2:46

Our rules say that word alone isn’t enough. He has no deadline and no safety risk. So the right answer is: normal.

2:54

The expert, mini, says normal. Right. The junior, nano, says high. Wrong.

3:00

On the same hundred and twenty emails, with no outage, how often did each one get all four boxes right?

3:09

The expert: ninety-seven percent. The junior: seventy-four.

3:15

And most of the junior’s misses were this same box: how urgent.

3:19

So an outage costs quality too, for as long as the junior is answering.

3:2504 · A circuit breaker

3:26

The fallback still has a waste. During the outage, every email phones the broken expert first.

3:32

And the OpenAI library redials twice on its own. That’s a hundred and forty-eight calls to a broken model.

3:40

A circuit breaker stops that. It’s like the switch in a fuse box: when something keeps failing, it trips, and cuts the line.

3:49

Mine trips after five failed emails within ten seconds. An email only counts as failed after all its redials. Tripped is called open. While it’s open, emails skip the expert, and go straight to the junior.

4:03

Five seconds later, it lets one test call through. If the expert answers, it closes again. If not, it stays open.

4:1205 · Breaker vs busy tone

4:14

Against the busy tone, the breaker tripped about three seconds into the outage. The red strip on top means open.

4:22

Waits stayed short either way: a busy tone comes back fast. The typical wait even fell by more than half.

4:30

The real win: the broken expert got thirty calls instead of a hundred and forty-eight. It’s left alone to recover.

4:3906 · No answer

4:40

Now the outage I opened with. The expert picks up, and says nothing. The phone just rings. That’s called hanging.

4:48

My code lets it ring for five seconds, then hangs up. That’s called a time limit.

4:53

With only the expert, Marcus’s email failed after sixteen seconds.

4:59

With the backup, the junior did answer him. But only after eighteen seconds.

5:05

He wasn’t alone: more than half the emails sent during the outage waited over seventeen seconds.

5:12

The blue ones, sent late, waited it out: their last redial came after the expert recovered.

5:1907 · Add the breaker

5:21

Now add the breaker. How long did Marcus wait this time?

5:27

Almost the same. Seventeen point six seconds.

5:31

The breaker did trip. But only with seven seconds of the outage left.

5:37

Most emails sent before the trip still waited about seventeen seconds. Only the ones sent after it tripped got fast answers.

5:4608 · Why so late

5:47

Here’s why. A hanging call rings for five seconds, before my code hangs up.

5:52

Then the library redials. Twice. Three rings, plus short pauses: about sixteen seconds before one failure is counted.

6:01

The breaker needs five failures to trip. The first one only arrived sixteen seconds into the outage. The fifth was most likely Marcus’s own email.

6:12

A breaker can only learn as fast as failures arrive. Here, each one took sixteen seconds.

6:2009 · Fail fast

6:21

So I made calls to the expert fail fast. Ring for three seconds, hang up, and don’t redial. The junior keeps its redials.

6:30

Now a failure arrives in about three seconds. The breaker tripped four and a half seconds into the outage.

6:37

So how long did Marcus wait this time?

6:42

About four seconds, instead of eighteen.

6:4710 · The price

6:48

And not just Marcus. The typical wait, the middle email of the outage, fell from seventeen seconds to about one.

6:57

The price: the junior answered about twenty more emails. Some came even after the outage, before the breaker had tested the expert again.

7:06

And three seconds is tight. One healthy call, sent just before the outage, was slow. It got cut off, and counted as a failure.

7:16

Across all the emails, the share with all four boxes right went from ninety-one to eighty-eight percent. On a test this small, that could be chance.

7:2711 · What I do now

7:28

So here’s what I do now. Every request gets a fallback chain, ending in a reply that’s always safe.

7:35

I set each time limit from how long a customer can wait. The library’s default is ten minutes.

7:42

In front of a breaker, the expert fails fast. Then the breaker hears about trouble in seconds.Sofia: Fail fast.

7:49

And some emails never go to the junior: a safety report, or a double charge. Those wait for the expert, or go to a person.

7:58

Next time: I kill my AI agent in the middle of a refund. Does the customer get paid twice?

8:05Summary

8:07

A fallback keeps you answering, with a weaker model.

8:10

A breaker keeps you fast, but only if the main model fails fast.Sofia: Thanks!

Key takeaways

  • ✓A fallback chain tries the next option when one fails: gpt-5.4-mini → gpt-5.4-nano → a canned “a person will reply”. Against a 25-second outage where every call errored at once, it turned 48 errors into 48 answers. The canned reply was never needed: nano never failed.
  • ✓An outage is a quality incident too. On the same 120 emails with no outage, mini got all four fields right on 97% and nano on 74%, mostly by misjudging how urgent an email is. Every email the backup answers is answered worse; decide before the outage which requests may take it.
  • ✓A circuit breaker counts failures and, after 5 failed emails in 10 s, stops calling the broken model (open), then lets one test call through 5 s later. Against instant errors it tripped 3.3 s into the outage, halved the typical wait (2.4 s → 1.0 s), and cut calls to the broken model from 148 to 30.
  • ✓Against a hang the breaker barely helped: Marcus waited 18.0 s with the backup and 17.6 s with the breaker too. Each failure took about 16 s to be counted (3 attempts × a 5 s limit, plus the SDK's backoff), so the breaker opened at 33.4 s, with 6.6 s of the outage left. A breaker learns only as fast as failures arrive.
  • ✓Making the main model fail fast (3 s, no retries; the backup keeps its retries) let the breaker trip 4.5 s into the hang. Marcus waited 4.3 s, and the typical wait during the outage fell from 17.2 s to 1.2 s. The price: nano answered 65 emails instead of 44, mini came back 6.7 s after recovery instead of 2.7, and one slow but healthy call was cut off and counted as a failure.
  • ✓Set every time limit from what the customer can wait (the OpenAI SDK's default is 600 s), end every chain in a reply that's always safe, fail fast in front of a breaker, and keep some requests (a safety report, a double charge) away from the weaker model.

Retries fix a short blip (chapter 32). They can't fix an outage: when the model is down for 25 seconds, every attempt in those 25 seconds fails together. This chapter adds somewhere else to send the request (a fallback chain), measures what that costs in quality, adds a circuit breaker to stop calling the broken model, and finds the case where the breaker barely helped: a model that hangs instead of erroring.

The source is Lab 13 (Resilience), script 02_fallbacks.py, recorded in October 2026 against the real OpenAI API. The workload is the support-email triage from the fine-tuning labs: 120 made-up emails with known right answers, 2 a second, each sorted by gpt-5.4-mini into four fields (what the customer wants, the order number, the product, and how urgent it is). From 15 s to 40 s, mini is broken on purpose, under the real SDK: either every call gets a 503 at once (a busy tone) or every call hangs (a phone that rings with no answer). Everything else is a real API call. The lab's other scripts are in the next chapters: crash recovery in chapter 34, and checking outputs and broken search in chapter 35. Its deadline script (one time budget across a four-step pipeline) isn't in the video.

A fallback chain

A fallback chain tries the next option when one fails:

gpt-5.4-mini ──fails──▶ gpt-5.4-nano ──fails──▶ canned reply: "a person will get back to you"

The last step never fails and is always safe: it doesn't triage at all, it hands the email to a person. The lab's version is a few lines:

def fallback(options, canned=None):
    errors = []
    for name, fn in options:
        try:
            return {"served_by": name, "result": fn(), "errors": errors}
        except Exception as e:          # every failure moves to the next option
            errors.append((name, type(e).__name__))
            last = e
    if canned is not None:
        return {"served_by": "canned", "result": canned, "errors": errors}
    raise last

Against the busy tone, with mini alone, 48 emails got an error: almost every one of the 50 sent during the outage. Marcus's email (he wants to return a sleeping pad, sent at 17 s) got its error after 1.3 s. With the chain, all 48 became answers, from nano, and not one error. Marcus got his after 2.4 s. The canned reply was never needed: nano never failed.

A model gateway (LiteLLM, Portkey, OpenRouter, or a cloud provider's own) runs this same chain outside your code, often across providers.

The backup is worse

Marcus writes “URGENT” in capitals. The shop's rules say that word alone isn't enough: he has no deadline and no safety risk, so the right urgency is normal. mini says normal. nano says high.

On the same 120 emails with no outage, all four fields right:

Answered by All 4 right
gpt-5.4-mini 97% (92–99%)
gpt-5.4-nano 74% (66–81%)
canned reply 0%: it doesn't triage

Most of nano's misses (28 of 31) were the same field: how urgent. So an outage is a quality incident too, for as long as the backup is answering. Decide before the outage which requests can take the cheaper model's answer.

A circuit breaker

During the outage, every email still phones the broken model first, and the OpenAI SDK retries twice on its own: 148 calls to a model that's down. A circuit breaker sits in front of mini and counts failures, like the switch in a fuse box:

closed ──(5 failed emails in 10 s)──▶ open ──(5 s later)──▶ half-open ──(test call works)──▶ closed
                                        ▲                        │
                                        └───(test call fails)────┘
  • Closed: calls go through.
  • Open: calls fail at once with CircuitOpen, without touching the service. In the chain, the email goes straight to nano.
  • Half-open: after the cooldown, one test call (a probe) goes through. If it works, the breaker closes; if not, it opens again for another 5 s.

An email counts as failed only after all its retries, and only for errors that can go away on their own (a timeout, a 5xx). A 400 is the caller's fault and doesn't count.

Against the busy tone, the breaker tripped 3.3 s into the outage. Waits were short either way, since an error comes back fast, but the typical wait during the outage still fell from 2.4 s to 1.0 s. The real win: the broken model got 30 calls instead of 148, and was left alone to recover.

When the model hangs

Now the other outage: mini picks up and says nothing. The client waits for its time limit, here 5 s, then gives up, and the SDK retries twice.

  • With mini alone, Marcus's email failed after 16.3 s.
  • With the fallback, nano answered him, after 18.0 s. More than half of the 50 emails sent during the outage (28) waited over 17 s.
  • With the fallback and the breaker: 17.6 s. Almost the same.

The breaker did trip, at 33.4 s: with 6.6 s of the outage left. Most emails sent before the trip still waited about 17 s; only the ones sent after it got fast answers. Emails sent late in the outage and answered by mini waited it out: their last retry came after mini recovered.

Why the breaker was late

A breaker can only count failures that have happened. Here, one failure takes:

3 attempts × 5 s time limit  +  the SDK's backoff between them (0.5 s, then 1 s, minus jitter)  ≈ 16.3 s

The first failure arrives 16 s into the outage. The breaker needs five; emails arrive 2 a second, so the fifth lands about 2 s after the first: 15 + 2 + 16.3 ≈ 33.3 s. It opened at 33.4 s, and the fifth failure was most likely Marcus's own email. A breaker learns only as fast as failures arrive. The same arithmetic gives 18.3 s for the busy tone, where each attempt errors at once and only the backoff is left; the breaker opened at 18.3 s.

Change the time limit, the retries and the trip count, and see when the breaker would have tripped:

Try it · When does the breaker trip?The timing behind script 02, checked against its runs

How mini breaks

Time limit per attempt

Retries inside the SDK

Failed emails to trip

One failure takes
16.3 s
Trips, into the outage
18.3 s
Measured in the lab
18.4 s
0 soutage ends, 25 s

Amber: the first email sent into the outage, until it's counted as failed. Red: the trip.

One failure = 3 attempts × 5 s + 1.3 s of SDK backoff. Emails arrive 2 a second, so the 5th failure lands 2.0 s after the first. Until the trip, every email waits for mini to fail before nano is even asked. Lab run: fallback + breaker, hang.

Fail fast

So mini was made to fail fast: a 3 s time limit and no retries. nano keeps its 5 s and its two retries, so the backup is still robust. Now a failure arrives in about 3 s, and the breaker tripped 4.5 s into the hang. Marcus waited 4.3 s instead of 17.6, and the typical wait during the outage fell from 17.2 s to 1.2 s.

All eight runs, email by email. Each row is one email, from sent to answered:

Try it · The outage, email by emailLab 13 · script 02 · 120 emails, 2 a second, mini down 15–40 s

How mini breaks

What the code does

The same chain, with a breaker in front of mini: open after 5 failed emails in 10 s, test again 5 s later.

0 s15 s40 s60 s80 s
mini answered nano (the backup) answered error breaker open (faint: testing) outage
mini / nano / error
76/44/0
All 4 right
91%
Typical wait, outage
17.2 s
Calls to broken mini
102

Marcus (the outlined row, sent at 17 s): answered by nano after 17.6 s. The breaker tripped 18.4 s into the outage, with 6.6 s of it left. mini answered again 2.7 s after it recovered.

Typical wait: the median over the 50 emails sent during the outage. Calls to mini count every attempt, the SDK's retries included. All 4 right is over all 120 emails; with no outage mini gets 97% and nano 74%. The canned reply was never needed: nano never failed.

Outage Code mini / nano / error All 4 right Typical wait, outage Marcus Calls to mini, outage
busy tone mini only 72 / 0 / 48 58% 1.4 s error, 1.3 s 148
busy tone fallback 72 / 48 / 0 93% 2.4 s 2.4 s 148
busy tone fallback + breaker 60 / 60 / 0 89% 1.0 s 3.0 s 30
busy tone fail fast + breaker 58 / 62 / 0 88% 1.0 s 1.1 s 10
hang mini only 92 / 0 / 28 72% 16.2 s error, 16.3 s 118
hang fallback 92 / 28 / 0 93% 17.3 s 18.0 s 118
hang fallback + breaker 76 / 44 / 0 91% 17.2 s 17.6 s 102
hang fail fast + breaker 55 / 65 / 0 88% 1.2 s 4.3 s 12

The price of failing fast

  • More emails on the weaker model: nano answered 65, not 44. Without failing fast, slow retries outlasted the outage and mini answered late but well. Some of nano's answers came even after mini had recovered, while the breaker was still open or testing (13 of the 14 that came after 40 s, before 46.7 s).
  • A slower return: mini answered again 6.7 s after it recovered, against 2.7 s with the plain breaker.
  • Three seconds is tight. One healthy call, sent just before the outage, was slow, got cut off at 3 s, and counted as one of the five failures. Without it, the fifth real failure was Marcus's, at 20.0 s.
  • Quality: across all 120 emails, all 4 right went from 91% to 88%. On a test this small that could be chance (p = 0.51).

So pick per request type: 4 s answers from the backup, or 18 s answers from the main model.

Caveats

  • One run per setting, 120 emails, one injected outage. The outage starts and ends sharply; real ones flap, slow down gradually, or hit some requests and not others.
  • The failures are injected at the HTTP layer, under the real SDK, so retries, the chain and the breaker all see real exceptions. Everything not broken on purpose is a real call to the API.
  • The emails are made up, with known right answers, so quality can be scored. A real shop's mix of urgent emails decides how much the backup costs.
  • The timing arithmetic is an average. The SDK's backoff has jitter, and calls in flight when the outage starts finish at different times; it matched the measured trips to within half a second.
  • Not tested here: a gateway, hedged requests (send a second request if the first is slow), and a breaker that treats rate limits (429) differently from outages. They're the lab's exercise ideas.

The checklist

  1. Give every model call a fallback chain ending in a reply that's always safe.
  2. Measure the backup's quality before you need it, on the same items as the main model.
  3. Choose which requests never take the backup (a safety report, a double charge): queue them for the main model or a person, and tell the customer.
  4. Set every time limit from what the customer can wait. The OpenAI SDK's default is 600 s per attempt, with 2 retries.
  5. Put a breaker in front of a service that can go down, counting only errors that can go away on their own.
  6. In front of a breaker, fail fast: a short limit and no retries on the main model, retries on the backup. Then the breaker hears about trouble in seconds.
  7. Test with both kinds of outage, an instant error and a hang. A design that works for one can fail the other.

Practice: build the chain and the breaker

Write the lab's fallback chain and circuit breaker (closed, open, half-open, driven by a fake clock), then the arithmetic of why it tripped late: how long one failure takes to be counted, and when the fifth one lands. The last test checks the recorded waits from the hang outage: a typical 17.2 s with the breaker, 1.2 s failing fast, and Marcus's 18.0, 17.6 and 4.3 s.

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

Quiz

Try it · Check your understanding0 of 6 answered
  1. 1.Retries fixed short blips in the last chapter. Why don't they help in a 25-second outage?

  2. 2.With the fallback chain, all 120 emails got an answer. What did that cost?

  3. 3.Against instant errors (a 503 on every call), what was the breaker's main win?

  4. 4.Why did the breaker trip with only 6.6 s of the 25-second hang left?

  5. 5.What changed when mini was made to fail fast (3 s, no retries)?

  6. 6.What's the price of failing fast?

Check your understanding

Try each question before opening its answer.

What does a fallback chain buy you, and what does it cost?

It keeps answering: against instant errors it turned 48 errors into answers. The cost is quality, for as long as the backup answers: with no outage, mini got 97% of emails fully right and nano 74%.

What does a circuit breaker add when the fallback already removes the errors?

It stops calling a service that's known to be down. Against instant errors, calls to the broken model fell from 148 to 30, and the typical wait from 2.4 s to 1.0 s, since emails skip the doomed first try.

Why did the breaker barely help when mini hung?

Each failure took about 16.3 s to be counted: 3 attempts × a 5 s time limit plus the SDK's backoff. The breaker needs 5 failures, so it opened at 33.4 s, with 6.6 s of the 25-second outage left. Marcus waited 17.6 s instead of 18.0.

What does "fail fast" mean here, and what did it change?

The main model gets a 3 s limit and no retries; the backup keeps its retries. A failure now arrives in about 3 s, the breaker tripped 4.5 s into the hang, Marcus waited 4.3 s, and the typical wait fell from 17.2 s to 1.2 s.

What did failing fast cost?

nano answered 65 emails instead of 44, mini came back 6.7 s after recovery instead of 2.7 s, and one slow but healthy call was cut off and counted as a failure. All 4 right fell from 91% to 88%, which on 120 emails could be chance (p = 0.51).

Which requests should never go to the backup model?

The ones where a wrong answer is expensive, such as a safety report or a double charge. Queue them for the main model or a person, and tell the customer it will take longer.

Next up: Chapter 34 kills an AI agent in the middle of a refund. Does the customer get paid twice?

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.