Overview
A circuit breaker wraps calls to a dependency and stops calling it when it is failing, so the caller fails fast instead of waiting on timeouts and piling up threads. Like an electrical breaker, it has three states: closed (calls flow normally while failures are counted), open (calls are rejected immediately for a cool-down period), and half-open (a few trial calls test whether the dependency has recovered).
Circuit breakers protect both sides: the caller keeps its resources free, and the struggling dependency gets breathing room to recover instead of being hammered by retries. They are usually combined with timeouts, fallbacks, and metrics.
When too much current flows, the breaker trips and cuts power to protect the wiring. After a while, you try flipping it back on; if the fault is fixed, power flows again, otherwise it trips again.
When to use it
- Calls to remote services that can fail or become slow.
- Preventing cascading failures in microservices.
- Third-party APIs with outages.
- Anywhere retries could overwhelm a struggling dependency.
Where it shows up in interviews
Recognize it when: a downstream outage causes timeouts everywhere.
- Design a resilient product page
- Design a payment gateway integration
Where it is used in real software
Popularized circuit breakers for microservices; now in maintenance, replaced by Resilience4j.
Standard libraries for Java and .NET.
Ejects failing hosts from load balancing pools automatically.
Key terms
- Closed
- Normal operation; failures are counted.
- Open
- Calls fail immediately without reaching the dependency.
- Half-open
- Limited trial calls to test recovery.
- Failure threshold
- Error or slow-call rate that trips the breaker.
- Sliding window
- Recent calls used to compute the failure rate.
How it works, step by step
- 1Closed: count outcomes
Track failures and slow calls in a sliding window.
- 2Trip to open
When the failure rate exceeds the threshold (with a minimum number of calls).
- 3Open: fail fast
Return a fallback or error immediately.
- 4After the wait time: half-open
Allow a few trial calls.
- 5Close or re-open
Trials succeed: close; fail: open again.
STEP 1Closed: calls pass through. 3 of the last 20 calls failed (15%), below the 50% threshold.
Timeline of an outage with a breaker
Threshold 50% over 20 calls, open for 30 s
| Time | Dependency | Breaker | Caller experience |
|---|---|---|---|
| 0 s | Healthy | Closed | Normal responses |
| 10 s | Failing | Closed, counting | Some requests wait for timeouts |
| 12 s | Failing | Open | Instant fallback, threads freed |
| 42 s | Recovered | Half-open | Trial calls succeed |
| 43 s | Healthy | Closed | Normal responses |
NOWTime: 0 s | Dependency: Healthy | Breaker: Closed | Caller experience: Normal responses
Without the breaker, every request during the outage would wait for a timeout, exhausting threads and spreading the failure.
Implementation
type State = "closed" | "open" | "half-open"; export class CircuitBreaker { private state: State = "closed"; private results: boolean[] = []; private openedAt = 0; private trials = 0; constructor(private opts = { window: 20, minCalls: 10, failureRate: 0.5, openMs: 30_000, halfOpenTrials: 3 }) {} async call<T>(fn: () => Promise<T>, fallback: () => T): Promise<T> { if (this.state === "open") { if (Date.now() - this.openedAt < this.opts.openMs) return fallback(); this.state = "half-open"; this.trials = 0; } if (this.state === "half-open" && this.trials >= this.opts.halfOpenTrials) return fallback(); if (this.state === "half-open") this.trials++; try { const value = await fn(); this.record(true); return value; } catch { this.record(false); return fallback(); } } private record(ok: boolean) { if (this.state === "half-open") { if (!ok) return this.trip(); if (this.trials >= this.opts.halfOpenTrials) { this.state = "closed"; this.results = []; } return; } this.results = [...this.results.slice(-(this.opts.window - 1)), ok]; const failures = this.results.filter((r) => !r).length; if (this.results.length >= this.opts.minCalls && failures / this.results.length >= this.opts.failureRate) this.trip(); } private trip() { this.state = "open"; this.openedAt = Date.now(); }}Complexity and performance
Counter updates.
Needs minimum volume.
Trade-offs
Low thresholds trip on brief blips and cause unnecessary fallbacks; high thresholds react too slowly.
Per-service breakers can open because of one bad host; per-host (outlier detection) is more precise.
Variants and related techniques
Trip on latency, not only errors.
Reduce allowed in-flight requests as latency rises (Netflix concurrency-limits).
Common mistakes
- Counting client errors (400s) as failures.
Fix: Only count dependency failures (timeouts, 5xx).
- No fallback strategy.
Fix: Decide what to return when open: cached data, default, or clear error.
- Breakers without monitoring.
Fix: Emit state changes as metrics and alerts.
Interview questions
Explain the circuit breaker pattern.
A wrapper that counts failures of calls to a dependency. When failures exceed a threshold it opens and rejects calls immediately for a cool-down, then half-opens to test with a few calls, closing if they succeed. It prevents wasting resources on a failing dependency and lets it recover.
How do retries and circuit breakers interact?
Retries handle brief transient errors; the circuit breaker stops retries from hammering a dependency that is persistently failing. The breaker usually wraps the retry logic so an open breaker short-circuits all attempts.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Implement a circuit breaker with half-open state | Medium | State machine. |
| Choose thresholds for a payment provider breaker | Medium | Sensitivity. |