Pranav Srivastava

11 lessons

0/11 done
Lesson 6 of 11·11 min·Intermediate
511 min

The Circuit Breaker

What you will learn
  • Describe the three breaker states and what moves between them
  • Explain why naive retries make outages worse, and what jitter fixes
  • Implement a breaker in about 30 lines
  • Decide what an agent should do while a breaker is open

The scare

A search service Pip depends on starts timing out. Pip's code does the sensible-sounding thing: if it fails, try again. Immediately. Repeatedly.

Now imagine a hundred Pips. The service is struggling, and a hundred clients are each asking it again, and again, as fast as they can. Each retry is more work for a service that has none to spare. It falls over harder. Engineers call this a retry storm: the original problem was small and short, but the retries keep the outage alive.

This is not hypothetical. Amazon's engineers have written about exactly this in their Builders' Library. Their point about depth is the scary bit: if every layer of a system retries three times and the failing service is five layers down, it can end up seeing hundreds of times its normal load. Nobody planned that. Every layer just tried to be resilient.

Refresher: why retries can make things worse

Play with this. A hundred agents, one service that is down for the first three seconds. The dashed line is what it can handle; go far above it and it collapses instead of merely slowing.

100 agents, one shaky service (down for 3 seconds; handles 40/sec, and collapses past 80)
0srequests per second (dashed line = what the service can handle)16s

Peak load: 100 requests/sec · still waiting at the end: 100 of 100

Everyone retries on the same beat. The service comes back, gets hit by all 100 at once, and falls over again. It never gets a chance to recover.

Pick each strategy in turn. Retrying instantly, the service never recovers. Backoff on its own is better on paper, but everyone waits the same time, so they return in a synchronised wave. Adding jitter, a bit of randomness in the wait, breaks up the wave. That one small idea is the reason this simulation ends happily.

The pattern

A circuit breaker takes it further: stop calling altogether once a dependency is clearly down. The name comes from the fuse box. Too much current, the breaker trips, the wiring does not burn.

Breaker state machine
            failures reach threshold
   CLOSED ────────────────────────────► OPEN
   (calls flow)                         (calls refused instantly)
      ▲                                    │
      │ trial call succeeds                │ cooldown elapsed
      │                                    ▼
      └─────────────────────────────── HALF-OPEN
                trial call fails ──►   (one test call allowed)
                back to OPEN

Try it

Now you are the flaky API. Fail three calls to trip it, then wait out the cooldown.

Be the flaky API — trip the breaker (3 failures opens it)

closed

Calls pass through. Failures are counted.

Failures: 0/3

  • › Breaker is closed. Try a failing call.

Build it

import time

class CircuitOpen(Exception):
    pass

class Breaker:
    def __init__(self, threshold=3, cooldown=30):
        self.threshold, self.cooldown = threshold, cooldown
        self.failures, self.opened_at = 0, None

    def call(self, fn, *args, **kwargs):
        if self.opened_at is not None:
            if time.time() - self.opened_at < self.cooldown:
                raise CircuitOpen("dependency is down; try the fallback")
            # cooldown over: half-open, let this one call through as a trial
        try:
            result = fn(*args, **kwargs)
        except Exception:
            self.failures += 1
            if self.opened_at is not None or self.failures >= self.threshold:
                self.opened_at = time.time()      # (re)open
            raise
        self.failures, self.opened_at = 0, None   # success closes it
        return result

search_breaker = Breaker(threshold=3, cooldown=30)

def flaky_search(q):
    raise TimeoutError("search API timed out")

for _ in range(5):
    try:
        search_breaker.call(flaky_search, "refund policy")
    except CircuitOpen as e:
        print("skipped:", e)          # calls 4 and 5 never touch the API
    except TimeoutError:
        print("failed")

Calls 4 and 5 never leave the building. The failing service gets breathing room, and Pip stops burning tokens on a lost cause.

What the agent does while it is open

An open breaker is information, so pass it on. Return "search is unavailable for about 30 seconds" and let Pip do something sensible: answer from what it already has, say so honestly, or take the next rung on the Fallback Ladder. What it must not do is loop.

When it goes wrong

  • Threshold of 1. A service that hiccups once a day now blocks everything for 30 seconds each time.
  • Nobody watches it. A breaker that opens silently looks like a mysterious slowdown. Log every state change to the trace.
  • Counting the wrong failures. A 400 "bad request" is your bug, not the service's outage. Only trip on timeouts and 5xx.
  • Retrying inside every layer. Retry in one place. See the depth problem above.

The agent-loop cousin

The same idea works inside the loop. If the agent has made the same call with the same arguments three times and got the same error each time, trip a breaker on that pattern. This is the practical end of Loop Engineering: a loop must always be able to stop.

Chapter summary
  • Retrying a failing dependency can make an outage bigger, especially when every layer retries
  • Backoff plus jitter spreads retries out; a breaker stops them altogether
  • Closed passes calls, open refuses them, half-open tests once
  • Give the model a clear "unavailable" message so it can adapt, and keep one breaker per dependency
Check your understanding
  1. Your breaker threshold is 1. What goes wrong with a dependency that fails once a day for no reason?
  2. What should Pip tell the customer while the model provider's breaker is open?

Finished this lesson?

Mark it done — your progress is saved automatically.