~/software-design/circuit-breaker-pattern-explained.md

The Circuit Breaker Pattern: Fail Fast, So One Dead Service Doesn't Kill Them All

4 min readNovember 03, 2026by Gorav Singal#circuit-breaker#resilience#system-design
// TL;DR

A circuit breaker protects your service from a failing dependency. It is a small state machine with three states: CLOSED (calls pass through, but failures are counted), OPEN (once failures cross a threshold, every call FAILS FAST instantly without even trying the dead service), and HALF-OPEN (after a cooldown, let ONE trial call through — if it succeeds, reset to closed; if it fails, go back to open). Why it matters: without a breaker, one slow dependency (say 10s per call) makes every caller's threads block waiting on it, and that pressure cascades up the chain A→B→C→D until the whole system is down — a cascading failure. With a breaker, the moment the dependency is unhealthy, calls to it fail fast with a FALLBACK (cached value, default, or queue it), so the failure stays contained. Pair it with a TIMEOUT, RETRIES with backoff (idempotent calls only), a BULKHEAD to isolate thread pools, and a FALLBACK — and watch the breaker's state in your metrics, because a flapping breaker is an early warning.

The Circuit Breaker Pattern: Fail Fast, So One Dead Service Doesn't Kill Them All

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.

Why you need a circuit breaker, stopping the cascade. Without a breaker: service D slows to 10 seconds per call, so C's threads all block waiting on D, then B's threads block waiting on C, then A's threads block waiting on B — one dead leaf brings the whole chain A to B to C to D down in a cascading failure. With a breaker on D: D crosses the failure threshold, C's breaker trips OPEN so calls to D fail in 1 millisecond with a fallback such as a cached value, a default, or queueing it, so C stays up and so do B and A, and the failure is contained to just the D feature. Pair the breaker with a timeout, retries with backoff on idempotent calls only, a bulkhead to isolate thread pools, and a fallback, and watch its state in metrics.

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.

Circuit breaker, the three states. CLOSED: calls pass through normally, but the breaker counts failures; when failures cross a threshold such as 50 percent of the last 20 calls, it trips to OPEN. OPEN: every call fails fast instantly without even trying the dead service, which stops wasting threads and time; after a cooldown it moves to HALF-OPEN. HALF-OPEN: let one trial call through — if it succeeds the service is back so reset to CLOSED, if it fails go back to OPEN for another cooldown. It is a simple state machine with a huge impact: the difference between one slow dependency and a dead system.

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

Share

Related Posts

The Transactional Outbox Pattern: Never Lose an Event Again

The Transactional Outbox Pattern: Never Lose an Event Again

This is one of those patterns that looks like nothing until you get burnt once…

Event Sourcing, Explained: Store What Happened, Not Just the Final State

Event Sourcing, Explained: Store What Happened, Not Just the Final State

Here is a question I like to ask in interviews: your bank account shows a…

CQRS Pattern, Explained Simply: Split Your Writes From Your Reads

CQRS Pattern, Explained Simply: Split Your Writes From Your Reads

Most of us start our career with one model for everything. The same class, the…

Change Data Capture (CDC): Stream Every Database Change Without Touching Your App

Change Data Capture (CDC): Stream Every Database Change Without Touching Your App

Your orders live in Postgres. But you also need them in Elasticsearch for search…

System Design Patterns for Managing Long-Running Tasks

System Design Patterns for Managing Long-Running Tasks

Introduction Some operations simply can’t finish in the time a user is willing…

Explaining SAGA Patterns with Examples

Explaining SAGA Patterns with Examples

In a monolith, placing an order is a single database transaction — deduct…

Latest Posts

How to Stay Valuable as a Software Engineer in the AI Era: A Practical Playbook

How to Stay Valuable as a Software Engineer in the AI Era: A Practical Playbook

In the previous article I argued that AI is not killing software engineering…

How to Review AI-Generated Code: A Checklist for Catching What the Model Missed

How to Review AI-Generated Code: A Checklist for Catching What the Model Missed

A few months back I approved a PR that was almost entirely AI-generated. It…

How to Debug AI-Generated Code: Understand First, Fix Last

How to Debug AI-Generated Code: Understand First, Fix Last

In the last article I wrote about reviewing AI code before it goes in. This one…

The Future of Software Engineering: What AI Changes, What Stays, and Where the Jobs Go

The Future of Software Engineering: What AI Changes, What Stays, and Where the Jobs Go

Every few weeks somebody sends me a scary headline — “AI will replace all…

The Transactional Outbox Pattern: Never Lose an Event Again

The Transactional Outbox Pattern: Never Lose an Event Again

This is one of those patterns that looks like nothing until you get burnt once…

Event Sourcing, Explained: Store What Happened, Not Just the Final State

Event Sourcing, Explained: Store What Happened, Not Just the Final State

Here is a question I like to ask in interviews: your bank account shows a…