System Design

Circuit breakers: fail fast instead of waiting on a dead service

TL;DR

A circuit breaker sits between a caller and its dependency. When failures cross a threshold it “opens” and fails subsequent requests immediately instead of letting them hit timeouts. After a cool-down it lets a trial request through to check whether the service has recovered.

Watch on Facebook

A dependency starts slowing down or dies. Every request to it waits for the full timeout (say 1 second) before failing. Meanwhile your threads, connections and memory are held up, queues grow, and your service slows down too: a local failure spreads into a cascading outage. A circuit breaker solves this by sitting between the caller and the dependency: when errors pass a threshold, it "trips" and fails subsequent requests immediately instead of letting them wait for the timeout.

Three states

StateBehaviourMoves to
ClosedRequests pass through normally; the breaker counts errors.Open when the error threshold is reached (e.g. 5 errors)
OpenFails immediately without calling the service, leaving it alone to recover.Half-open after a wait (e.g. 3 seconds)
Half-openLets 1 trial request through.Closed on success, Open again on failure

In the video: during the 3 seconds the circuit was open, 15 requests got an immediate response instead of each waiting 1000ms.

Why it helps

  • Fail fast: users get a response immediately instead of waiting for a timeout.
  • Protects your resources: no threads/connections tied up in calls that will surely fail.
  • Gives the dependency a chance to recover: less load while it's in trouble.
  • Isolates failures: stops them spreading to other parts of the system.

Sketch code

class Breaker {
  state = 'closed'; fails = 0; openedAt = 0;
  async call(fn) {
    if (this.state === 'open') {
      if (Date.now() - this.openedAt < 3000) throw new Error('circuit open');
      this.state = 'half-open';
    }
    try { const r = await fn(); this.state = 'closed'; this.fails = 0; return r; }
    catch (e) {
      if (++this.fails >= 5 || this.state === 'half-open') { this.state = 'open'; this.openedAt = Date.now(); }
      throw e;
    }
  }
}

Choosing parameters

  • Error threshold: consecutive errors or error rate within a time window; you need enough traffic for a rate to be meaningful.
  • Open duration: too short and you keep hitting a service that hasn't recovered; too long and the outage drags on.
  • Trial requests in half-open: few, to avoid overwhelming a recovering service.
  • Which errors count: only infrastructure errors/timeouts, not business errors like 404.

How to apply it

  • Put a breaker on every outbound call (third-party APIs, other internal services) so one dead service doesn't bring down the whole system.
  • Have a fallback when the circuit is open: return cached data, a default value or a friendly message.
  • Combine with short timeouts and retries with jitter; the breaker stops pointless retries when the service is down.
  • Monitor: alert when a breaker opens; it's an early incident signal.

Suggested libraries: Resilience4j (Java), Polly (.NET), opossum (Node.js), or a service mesh such as Istio.

Sources & references

  1. CircuitBreaker — Martin Fowler