Patterns for Humans software concepts, explained simply
← All concepts
pattern

Circuit Breaker

Stop hammering a service that's already down. Give it room to recover.

The simple version

If a downstream service starts failing, calling it over and over — hoping this time it works — usually makes things worse, not better. A circuit breaker watches for repeated failures and, past a threshold, stops calling entirely for a while, failing fast instead.

The analogy

It’s called a circuit breaker because it’s the literal electrical one, and the metaphor is almost too on-the-nose.

When a house circuit detects a dangerous surge, it doesn’t let current keep flowing and hope for the best — it trips, cutting the circuit immediately. That protects the wiring from actual damage. Crucially, it doesn’t stay tripped forever out of panic: after a while, someone flips it back — or in the electrical version, a fault-clearing mechanism allows a cautious retry — to see if the underlying problem is gone.

Software does the same thing on purpose: if a service downstream keeps failing, stop sending it traffic, protect your own system from piling up broken requests, then cautiously test the waters again later.

The bridge

A circuit breaker sits in front of a risky call and tracks its own state:

  • Closed — normal operation. Calls go through. Failures are counted.
  • Open — too many recent failures tripped the breaker. Calls fail immediately, without even attempting the network call, for a cooldown period.
  • Half-open — after the cooldown, let a small number of calls through as a test. If they succeed, close the breaker (back to normal). If they fail, snap back to open and wait again.

The value isn’t just “fewer failed calls” — it’s protecting the caller. Without a breaker, a slow or dead downstream service can pile up threads/connections waiting on timeouts until the caller itself falls over — a failure in one service cascading into a failure of the whole system. Failing fast (no network call at all, while open) keeps the caller healthy even while the dependency is down.

In code

class CircuitBreaker {
  private failures = 0;
  private state: "closed" | "open" | "half-open" = "closed";
  private openedAt = 0;

  constructor(
    private readonly threshold = 5,
    private readonly cooldownMs = 30_000
  ) {}

  async call<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === "open") {
      if (Date.now() - this.openedAt < this.cooldownMs) {
        throw new Error("Circuit open — failing fast, not calling downstream");
      }
      this.state = "half-open"; // cooldown elapsed, cautiously testing again
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (err) {
      this.onFailure();
      throw err;
    }
  }

  private onSuccess() {
    this.failures = 0;
    this.state = "closed";
  }

  private onFailure() {
    this.failures++;
    if (this.failures >= this.threshold) {
      this.state = "open";
      this.openedAt = Date.now();
    }
  }
}

const paymentBreaker = new CircuitBreaker();

async function chargeCard(order: Order) {
  return paymentBreaker.call(() => paymentGateway.charge(order));
}

Once paymentGateway fails 5 times in a row, further calls fail instantly with “circuit open” for 30 seconds — no wasted timeouts, no pile-up — then the breaker lets one call through to check if the gateway has recovered.

When you don’t need it

For calls that are cheap, fast to time out, and non-critical, a circuit breaker is just extra state to maintain for little benefit — a short timeout and a retry might be all you need. It earns its place around calls to external dependencies that are slow to fail (long timeouts), expensive to retry, or where a pile-up of failed calls could genuinely take your own service down with it.