Imagine an API you depend on goes down. Your app keeps trying to call it. Every request waits for timeout. Users experience 30-second hangs. Your connection pool fills up. Now your whole app is slow.
This is the cascading failure problem. Circuit breakers solve it.
The Pattern
Think of it like an electrical circuit breaker. When theres a problem, it "trips" to prevent damage:
Closed: Everything works. Requests flow through.
Open: Too many failures. Reject immediately without trying.
Half-Open: Test if service recovered. One request gets through.
Basic Implementation
class CircuitBreaker {
private state: 'closed' | 'open' | 'half-open' = 'closed';
private failures = 0;
private lastFailure: number = 0;
private readonly threshold = 5;
private readonly timeout = 30000; // 30 seconds
async call<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (Date.now() - this.lastFailure > this.timeout) {
this.state = 'half-open';
} else {
throw new Error('Circuit breaker is open');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failures = 0;
this.state = 'closed';
}
private onFailure() {
this.failures++;
this.lastFailure = Date.now();
if (this.failures >= this.threshold) {
this.state = 'open';
}
}
}
Using It
const paymentBreaker = new CircuitBreaker();
async function processPayment(amount: number) {
try {
return await paymentBreaker.call(() =>
paymentService.charge(amount)
);
} catch (error) {
if (error.message === 'Circuit breaker is open') {
// Fail fast - don't even try
throw new Error('Payment service temporarily unavailable');
}
throw error;
}
}
When Circuit Opens
The key insight: when the circuit is open, we fail immediately. No waiting for timeouts. No wasting resources.
Per-Service Breakers
Create separate breakers for each external service:
const breakers = {
payment: new CircuitBreaker({ threshold: 3, timeout: 30000 }),
inventory: new CircuitBreaker({ threshold: 5, timeout: 60000 }),
shipping: new CircuitBreaker({ threshold: 5, timeout: 60000 }),
};
async function checkout(order: Order) {
// Each service fails independently
const payment = await breakers.payment.call(() =>
paymentService.charge(order.total)
);
const stock = await breakers.inventory.call(() =>
inventoryService.reserve(order.items)
);
// If shipping is down, payment and inventory still work
const tracking = await breakers.shipping.call(() =>
shippingService.create(order)
);
}
Fallback Strategies
What to do when the circuit is open:
async function getProductPrice(productId: string) {
try {
return await priceBreaker.call(() =>
priceService.getPrice(productId)
);
} catch (error) {
// Fallback to cached price
const cached = await cache.get(`price:${productId}`);
if (cached) return cached;
// Or return a default
throw new Error('Price unavailable');
}
}
Monitoring
Track circuit state for observability:
class CircuitBreaker {
// ... existing code ...
getStats() {
return {
state: this.state,
failures: this.failures,
lastFailure: this.lastFailure,
};
}
}
// Expose metrics
app.get('/health/circuits', (req, res) => {
res.json({
payment: breakers.payment.getStats(),
inventory: breakers.inventory.getStats(),
shipping: breakers.shipping.getStats(),
});
});
Libraries
Dont reinvent the wheel for production:
- Node.js: opossum
- Java: Resilience4j
- Go: sony/gobreaker
Further Reading
- Martin Fowler on Circuit Breaker
- Release It! - Great book on stability patterns
Circuit breakers are about protecting your system from itself. When dependencies fail, fail fast and give them room to recover. Your users get quick errors instead of endless hangs, and the struggling service gets time to breathe.
