| name | circuit-breaker-pattern |
| description | Prevent cascading failures by short-circuiting failing services. Trigger: When calling external APIs, databases, or any unreliable dependency. |
| license | Apache 2.0 |
| metadata | {"version":"1.0","type":"domain"} |
Circuit Breaker Pattern
Protects a system from cascading failures by monitoring calls to an external dependency. When failures exceed a threshold, the circuit "trips" โ subsequent calls fail immediately without attempting the operation, giving the dependency time to recover.
Like an electrical circuit breaker: when current spikes (failures accumulate), the breaker opens to prevent damage. It then probes for recovery before restoring normal flow.
When to Use
- Calling external APIs, databases, third-party services, or microservices
- Any dependency that can be slow or unavailable โ not just HTTP calls
- Systems where one failing service should not bring down the whole application
- High-traffic services where queuing up failed requests causes cascading load
Don't use for:
- Local in-process function calls (no network boundary, no circuit breaker needed)
- Operations that must succeed or retry indefinitely (use retry + timeout instead)
- User-facing validation errors (4xx responses are not failures โ they are expected)
Critical Patterns
โ
REQUIRED: Three States
The circuit breaker is a state machine with three states.
CLOSED โโ(failures > threshold)โโโถ OPEN โโ(after timeout)โโโถ HALF_OPEN
โฒ โ
โโโโโโโโโโโโโโโโโ(probe succeeds)โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
OPEN โโโ(probe fails)โโโ
type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
โ
REQUIRED: Failure Threshold Configuration
Configure per-dependency, not globally. Use a time-window count or error rate, not raw cumulative count.
interface CircuitBreakerConfig {
failureThreshold: number;
windowMs: number;
recoveryTimeoutMs: number;
halfOpenProbes: number;
successThreshold: number;
}
const paymentBreaker = new CircuitBreaker({ failureThreshold: 3, windowMs: 30_000, recoveryTimeoutMs: 15_000 });
const inventoryBreaker = new CircuitBreaker({ failureThreshold: 10, windowMs: 60_000, recoveryTimeoutMs: 5_000 });
โ
REQUIRED: Fallback Strategy
An open circuit must return something useful โ never silently fail.
async function getProductPrice(productId: string): Promise<Money> {
try {
return await pricingBreaker.execute(() =>
pricingService.getPrice(productId)
);
} catch (error) {
if (error instanceof CircuitOpenError) {
return cachedPrices.get(productId) ?? DEFAULT_PRICE;
}
throw error;
}
}
async function getProductPrice(productId: string): Promise<Money> {
return await pricingBreaker.execute(() => pricingService.getPrice(productId));
}
Fallback options by priority:
- Cached value โ return last known good response
- Default/safe value โ a sensible degraded response (empty list, zero price with warning)
- Queue for retry โ enqueue the request for later processing
- Fail fast with clear error โ tell the user the feature is temporarily unavailable
โ
REQUIRED: Observability
Emit events on state transitions and expose metrics. Silent circuit breakers hide production issues.
class CircuitBreaker {
private onStateChange?: (prev: CircuitState, next: CircuitState) => void;
private transition(next: CircuitState): void {
const prev = this._state;
this._state = next;
this.onStateChange?.(prev, next);
console.log(`[CircuitBreaker:${this.name}] ${prev} โ ${next}`);
}
}
breaker.onStateChange = (prev, next) => {
metrics.increment('circuit_breaker.state_change', { from: prev, to: next, service: 'pricing' });
if (next === 'OPEN') alerting.trigger(`Circuit breaker opened for pricing service`);
};
Metrics to expose: current state, failure count in window, last transition time, request count (success/failure/rejected).
โ NEVER: Circuit Breaker Without Fallback
An open circuit that just throws an unhandled error provides no protection โ it just changes which error the user sees.
router.get('/products/:id/price', async (req, res) => {
const price = await pricingBreaker.execute(() => pricingService.getPrice(req.params.id));
res.json({ price });
});
router.get('/products/:id/price', async (req, res) => {
try {
const price = await pricingBreaker.execute(() => pricingService.getPrice(req.params.id));
res.json({ price });
} catch (e) {
if (e instanceof CircuitOpenError) {
res.json({ price: null, message: 'Pricing temporarily unavailable' });
} else { res.status(500).json({ error: 'Internal error' }); }
}
});
โ NEVER: Single Global Breaker
One breaker for all services means one slow API trips the breaker and blocks all other services.
const globalBreaker = new CircuitBreaker({ failureThreshold: 5 });
const price = await globalBreaker.execute(() => pricingService.get(id));
const inventory = await globalBreaker.execute(() => inventoryService.check(id));
const priceBreaker = new CircuitBreaker({ name: 'pricing', failureThreshold: 3 });
const inventoryBreaker = new CircuitBreaker({ name: 'inventory', failureThreshold: 10 });
HALF_OPEN Probe Logic
Allow a small number of test requests through. Reset to CLOSED on enough successes; trip back to OPEN on any failure.
private async executeInHalfOpen<T>(fn: () => Promise<T>): Promise<T> {
if (this._probeCount >= this.config.halfOpenProbes) {
throw new CircuitOpenError(this.name);
}
this._probeCount++;
try {
const result = await fn();
this._successCount++;
if (this._successCount >= this.config.successThreshold) {
this.transition('CLOSED');
this.reset();
}
return result;
} catch (error) {
this.transition('OPEN');
this.scheduleRecovery();
throw error;
}
}
Timeout โ Failure
Not all errors should trip the circuit. Distinguish between service failures and client errors.
private isFailure(error: unknown): boolean {
if (error instanceof HttpError) {
return error.statusCode >= 500;
}
return error instanceof NetworkError || error instanceof TimeoutError;
}
Decision Tree
External dependency call?
NO โ No circuit breaker needed
Dependency can fail or be slow?
YES โ Add circuit breaker
What should happen when open?
โ Cached value available? โ Return cache
โ Default safe response? โ Return default + log
โ Feature non-essential? โ Return null/empty + inform user
โ Feature essential? โ Fail fast with clear error + alerting
Circuit breaker vs retry vs timeout?
โ Retry: transient errors (network blip); same request might succeed
โ Timeout: set max wait per request; combine with circuit breaker
โ Circuit breaker: protect against sustained failure; stop hammering the dependency
โ Best practice: timeout + retry(2) + circuit breaker together
How to tune thresholds?
โ failureThreshold: start at 5 failures / 60s window; adjust per SLA
โ recoveryTimeoutMs: at least 2ร the typical recovery time of the dependency
โ halfOpenProbes: 2-3 is usually enough; more = slower recovery detection
Example
Minimal TypeScript circuit breaker implementation.
class CircuitOpenError extends Error {
constructor(name: string) { super(`Circuit breaker OPEN: ${name}`); }
}
class CircuitBreaker {
private _state: CircuitState = 'CLOSED';
private _failures = 0;
private _windowStart = Date.now();
private _openedAt = 0;
constructor(
private name: string,
private config: CircuitBreakerConfig,
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this._state === 'OPEN') {
if (Date.now() - this._openedAt >= this.config.recoveryTimeoutMs) {
this._state = 'HALF_OPEN';
} {
(.);
}
}
{
result = ();
.();
result;
} (error) {
.();
error;
}
}
(): {
(. === ) . = ;
. = ;
}
(): {
now = .();
(now - . > ..) {
. = ;
. = now;
}
.++;
(. >= ..) {
. = ;
. = now;
}
}
}
Edge Cases
Library vs manual implementation: For production use, prefer opossum (Node.js) or cockatiel rather than a home-grown implementation โ they handle edge cases (concurrent requests in HALF_OPEN, atomic state transitions, metrics hooks).
Distributed circuit breakers: In multi-instance deployments, each instance has its own breaker state. Use a shared state store (Redis) only if consistency across instances is required. Usually, per-instance breakers are sufficient.
Circuit breaker with bulkhead: Combine circuit breaker (stop calls on failure) with bulkhead (limit concurrent calls) for full resilience: bulkhead โ circuit breaker โ timeout โ retry โ actual call.
Fallback strategy when OPEN: Choose based on whether staleness is acceptable:
- Cached data acceptable: Return last-known-good response from an in-memory or Redis cache.
- Partial results acceptable: Return degraded response (e.g., empty recommendations list instead of 500).
- No fallback possible: Fast-fail immediately with a user-facing message; never hang.
Avoid retrying inside the fallback โ the circuit is OPEN precisely to prevent cascading load.
Observability: Log every state transition with service name and failure reason:
circuit_breaker{service="payments", state="OPEN", reason="timeout"} 1
Expose a gauge metric circuit_breaker_state (0=CLOSED, 1=OPEN, 2=HALF_OPEN). Alert when OPEN for >30s โ sustained OPEN indicates the downstream service has not recovered.
Testing HALF_OPEN: Wall-clock resetTimeout makes tests slow and flaky. Use a configurable timeout (inject via constructor or env var) and set it to 0ms in tests. Alternatively, expose a forceHalfOpen() method on the breaker for test-only state injection.
Resources