← Back
July 9, 2026Architecture7 min read
The circuit breaker pattern: stop cascading failures before they happen
Published July 9, 20267 min read
Our recommendation service went slow — 10-second timeouts instead of 200ms. Every API request that called it started taking 10 seconds. The connection pool filled up. Then the product page started timing out. Then the homepage. One slow microservice cascaded into a full outage. The circuit breaker pattern would have stopped this at the first service. Here is how it works.
How a circuit breaker works
CLOSED (normal) -> failures cross threshold -> OPEN (fast fail) OPEN -> after timeout -> HALF-OPEN (test one request) HALF-OPEN -> request succeeds -> CLOSED HALF-OPEN -> request fails -> OPEN
Implementation in TypeScript
typescript
enum CircuitState {
CLOSED = 'CLOSED',
OPEN = 'OPEN',
HALF_OPEN = 'HALF_OPEN',
}
interface CircuitBreakerOptions {
failureThreshold: number; // Failures before opening (default: 5)
successThreshold: number; // Successes to close from half-open (default: 2)
openTimeoutMs: number; // How long to stay open (default: 60000)
callTimeoutMs: number; // Max time for a call (default: 5000)
}
class CircuitBreaker {
private state = CircuitState.CLOSED;
private failureCount = 0;
private successCount = 0;
private lastFailureTime: number | null = null;
constructor(
private fn: (...args: unknown[]) => Promise,
private options: CircuitBreakerOptions = {
failureThreshold: 5,
successThreshold: 2,
openTimeoutMs: 60_000,
callTimeoutMs: 5_000,
}
) {}
async call(...args: unknown[]): Promise {
if (this.state === CircuitState.OPEN) {
if (Date.now() - (this.lastFailureTime ?? 0) > this.options.openTimeoutMs) {
this.state = CircuitState.HALF_OPEN;
this.successCount = 0;
} else {
throw new CircuitOpenError('Circuit is OPEN — call rejected');
}
}
try {
const result = await Promise.race([
this.fn(...args),
this.timeout(),
]) as T;
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private timeout(): Promise {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error('Call timed out')), this.options.callTimeoutMs)
);
}
private onSuccess(): void {
this.failureCount = 0;
if (this.state === CircuitState.HALF_OPEN) {
this.successCount++;
if (this.successCount >= this.options.successThreshold) {
this.state = CircuitState.CLOSED;
console.log('Circuit CLOSED — service recovered');
}
}
}
private onFailure(): void {
this.failureCount++;
this.lastFailureTime = Date.now();
if (
this.state === CircuitState.HALF_OPEN ||
this.failureCount >= this.options.failureThreshold
) {
this.state = CircuitState.OPEN;
console.log(`Circuit OPEN — ${this.failureCount} failures`);
}
}
getState(): CircuitState {
return this.state;
}
}
class CircuitOpenError extends Error {}
Using the circuit breaker
typescript
async function fetchRecommendations(userId: string): Promise {
const response = await fetch(`https://recommendations.internal/users/${userId}`);
if (!response.ok) throw new Error('Recommendations service error');
return response.json();
}
// Wrap with circuit breaker
const recommendationsBreaker = new CircuitBreaker(fetchRecommendations, {
failureThreshold: 3, // Open after 3 consecutive failures
openTimeoutMs: 30_000, // Try again after 30 seconds
callTimeoutMs: 2_000, // Fail fast after 2 seconds
successThreshold: 2,
});
// In your route handler
async function getProductPage(req: Request, res: Response) {
const product = await getProduct(req.params.id);
let recommendations: Product[] = [];
try {
recommendations = await recommendationsBreaker.call(req.userId);
} catch (error) {
if (error instanceof CircuitOpenError) {
// Circuit is open — fail gracefully with empty recommendations
console.log('Recommendations circuit open — using empty fallback');
} else {
// Actual call failed — this failure was counted
console.error('Recommendations service error:', error);
}
// Page renders without recommendations instead of timing out
}
res.json({ product, recommendations });
}
Library alternatives
bash
# Don't build from scratch in production — use a battle-tested library
npm install opossum # Node.js circuit breaker (most popular)
pip install pybreaker # Python circuit breaker
typescript
import CircuitBreaker from 'opossum';
const breaker = new CircuitBreaker(fetchRecommendations, {
timeout: 2000,
errorThresholdPercentage: 50, // Open if 50% of calls fail
resetTimeout: 30000,
});
breaker.on('open', () => console.log('Circuit opened'));
breaker.on('halfOpen', () => console.log('Circuit half-opened'));
breaker.on('close', () => console.log('Circuit closed'));
// Built-in fallback support
breaker.fallback(() => []); // Return empty array when open
const recommendations = await breaker.fire(userId);
The circuit breaker pattern is essential for any distributed system. Without it, slow downstream services cause thread/connection exhaustion upstream, which cascades. With it, failures are contained at the boundary of the failing service and the rest of the system degrades gracefully rather than collapsing.
Share this
← All Posts7 min read