Like the electrical switch in your home that it is named after, a circuit breaker exists for one reason: to stop a local problem from burning the whole house down. In software, the “house” is your system and the “fire” is one slow or dead dependency. This is probaly the single most important resilience pattern to understand, because distributed systems fail in a very particular, sneaky way — and the breaker is the standard defence.
The failure it prevents: the cascade
Let me paint the picture, because this is how real outages actually happen. You have a chain of services: A calls B, B calls C, C calls D. One fine day, service D becomes slow — not down, just slow, taking 10 seconds per call instead of 50ms.
Now watch what happens without any protection. C calls D and its thread just sits there, blocked, for 10 whole seconds. Requests keep coming, so more and more of C’s threads get stuck waiting on D. Soon C has no free threads left — C is now effectively down. And because B is calling C, B’s threads start blocking too. Then A. One slow leaf at the very bottom has taken down the entire chain. This is a cascading failure, and it is how a tiny problem becomes a total outage in minutes.
The root cause is that everyone keeps patiently waiting on something that is clearly not healthy. The circuit breaker’s job is to stop waiting.
The three states
A circuit breaker is just a small state machine that sits in front of a risky call and watches how it is going.
- CLOSED — the normal, healthy state. Calls pass straight through to the dependency, but the breaker is quietly counting failures (and slow calls that time out) in the background.
- OPEN — once failures cross a threshold (say, more than 50% of the last 20 calls failed), the breaker trips. Now every call fails instantly, without even attempting to reach the dead service. This is the entire point: C stops throwing threads into a black hole and instead returns immediately. No more blocking, no more cascade.
- HALF-OPEN — the dependency might have recovered, so after a cooldown the breaker allows exactly one trial call through. If it succeeds, happy days — the service is back, so the breaker resets to CLOSED. If it fails, back to OPEN for another cooldown. This way you probe gently instead of flooding a recovering service and knocking it over again.
A simplified sketch:
def call(self, fn, fallback):
if self.state == "OPEN":
if time.time() - self.opened_at > self.cooldown:
self.state = "HALF_OPEN" # time to test the water
else:
return fallback() # fail fast, don't even try
try:
result = fn() # the real call (with a timeout!)
self.on_success() # HALF_OPEN success → CLOSED
return result
except Exception:
self.on_failure() # too many failures → OPEN
return fallback()The impact is out of proportion to how simple it is. A dozen lines of state machine is the difference between “one feature is degraded” and “the whole site is down”.
It does not work alone
A circuit breaker is a team player, and in practice you combine it with a few friends:
- Timeout — this one is mandatory. Without a timeout, a “slow” call never actually fails, so the breaker never counts it and never trips. A call that takes 10s must time out at, say, 1s so it registers as a failure quickly.
- Fallback — when the breaker is open, return something graceful: a cached value, a sensible default, an empty list, or queue the work to do later. An open circuit should degrade teh feature, not throw errors in the user’s face.
- Retry with backoff — for the occasional one-off blip, a retry helps. But only retry idempotent calls (see the idempotency article), and never retry into an open breaker — that defeats the whole purpose.
- Bulkhead — isolate thread pools per dependency, like the watertight compartments in a ship. Then even if calls to D pile up, they can only drown D’s own little pool, not the threads your other features need.
Two practical cautions from experience. First, tune the thresholds carefully — too sensitive and the breaker trips on a normal, harmless blip and hurts you for no reason; too lax and it never protects you when you actually need it. Second, put the breaker’s state on a dashboard. A breaker that is flapping open and closed repeatedly is one of the best early warnings you can get that a dependency is becoming unhealthy, often before users notice anything is wrong. Do keep an eye on it.
Takeaways
- A circuit breaker stops a slow/failing dependency from causing a cascading failure that takes down the whole chain.
- Three states: CLOSED (pass, but count failures), OPEN (fail fast, don’t even try), HALF-OPEN (one trial call to test recovery).
- Always pair it with a timeout (so slow calls register as failures), a fallback (graceful degradation), retry-with-backoff (idempotent calls only), and a bulkhead (isolated thread pools).
- Tune thresholds sensibly and monitor the breaker’s state — a flapping breaker is an early warning.
This closes the system-design patterns series. See also idempotency, the Saga pattern, and the data patterns CQRS, Event Sourcing, CDC and the Outbox pattern. For the complete toolkit, the System Design Masterclass and Distributed Systems in Practice courses are the natural next step.










