What a circuit breaker is
A circuit breaker is a resiliency pattern that protects your application from cascading failures. It sits in front of a dependency and monitors calls. When failures exceed a threshold, it "opens" the circuit — subsequent calls fail immediately without hitting the downstream service. After a cooldown period, it enters "half-open" state, allowing a probe request through. If the probe succeeds, the circuit closes and normal operation resumes.
Why circuit breakers need slow failures
A circuit breaker doesn't trip on fast errors alone. It trips when the system is under stress — meaning errors are frequent and responses are slow. A service that fails instantly is actually less dangerous than one that fails after 3 seconds, because fast failures don't exhaust your connection pool or block your threads.
This is why mocks that throw immediately can't test circuit breaker behavior accurately. A mock that throws in 0ms won't exhaust your connection pool. A mock that throws after 3 seconds will. slowdep gives you the latency + error combination that actually triggers circuit breaker thresholds.
High error rate + high latency = circuit breaker trip condition. slowdep lets you control both. errorRate: 0.3 with p99: 3000 simulates a degraded service that's slow to fail.
Simulating the trip condition
Configure slowdep with enough error rate and latency to trip your circuit breaker's thresholds. A 30% error rate with p99 of 3s is typical of a degraded database or downstream service.
import { withLatency } from 'slowdep'; // simulates a degraded service: slow AND error-prone const degradedService = withLatency(realService.call, { p50: 500, // 500ms median — already slow p99: 3000, // 3s p99 — tail exhausts connection pools errorRate: 0.3, // 30% fail — above typical circuit breaker threshold }); // fire requests until the circuit trips async function tripCircuit(cb: CircuitBreaker, service: Function) { const calls = Array.from({ length: 20 }, () => cb.execute(() => service()).catch(() => {}) // suppress individual errors ); await Promise.all(calls); }
Testing the open state
After the circuit opens, subsequent calls must fail immediately — not after a timeout. This is the core value of the pattern: once the circuit is open, no time is wasted waiting for a downstream that's known to be unhealthy.
it('fails fast when circuit is open', async () => { const cb = new SimpleCircuitBreaker({ threshold: 5, timeout: 10000 }); const degraded = withLatency( async () => { throw new Error('downstream error'); }, { p50: 500, p99: 3000, errorRate: 1.0 } ); // trip the circuit with 5 failures for (let i = 0; i < 5; i++) { await cb.execute(() => degraded()).catch(() => {}); } expect(cb.state).toBe('open'); // open circuit must fail immediately const start = Date.now(); await expect(cb.execute(() => degraded())) .rejects.toThrow('circuit open'); const elapsed = Date.now() - start; expect(elapsed).toBeLessThan(10); // must be near-instant, not 500ms+ });
Testing the half-open recovery
After the cooldown period, the circuit enters half-open state. Set the service back to healthy (errorRate: 0) and verify the circuit closes after a successful probe.
it('closes circuit after successful probe', async () => { jest.useFakeTimers(); const cb = new SimpleCircuitBreaker({ threshold: 3, cooldown: 5000 }); // wrap a function we can control let shouldFail = true; const service = withLatency( async () => { if (shouldFail) throw new Error('down'); return 'ok'; }, { p50: 200, p99: 2000, errorRate: 0 } // errorRate=0; failure controlled above ); // trip the circuit for (let i = 0; i < 3; i++) { await cb.execute(() => service()).catch(() => {}); } expect(cb.state).toBe('open'); // advance past cooldown ? half-open jest.advanceTimersByTime(5001); expect(cb.state).toBe('half-open'); // "service recovers" shouldFail = false; // probe succeeds ? circuit closes const result = await cb.execute(() => service()); expect(result).toBe('ok'); expect(cb.state).toBe('closed'); jest.useRealTimers(); });
A minimal circuit breaker with full tests
Here's a minimal circuit breaker implementation and tests for all three states — closed, open, and half-open — using slowdep to control failure conditions.
// SimpleCircuitBreaker.ts type State = 'closed' | 'open' | 'half-open'; export class SimpleCircuitBreaker { state: State = 'closed'; private failures = 0; private openedAt = 0; constructor(private opts: { threshold: number; cooldown: number }) {} async execute<T>(fn: () => Promise<T>): Promise<T> { if (this.state === 'open') { const elapsed = Date.now() - this.openedAt; if (elapsed < this.opts.cooldown) throw new Error('circuit open'); this.state = 'half-open'; } try { const result = await fn(); this.failures = 0; this.state = 'closed'; return result; } catch (err) { this.failures++; if (this.failures >= this.opts.threshold) { this.state = 'open'; this.openedAt = Date.now(); } throw err; } } }
import { withLatency } from 'slowdep'; import { SimpleCircuitBreaker } from './SimpleCircuitBreaker'; describe('SimpleCircuitBreaker', () => { it('stays closed on success', async () => { const cb = new SimpleCircuitBreaker({ threshold: 3, cooldown: 1000 }); const healthy = withLatency(async () => 'ok', { p50: 5, p99: 50 }); await cb.execute(() => healthy()); expect(cb.state).toBe('closed'); }); it('opens after threshold failures with realistic latency', async () => { const cb = new SimpleCircuitBreaker({ threshold: 3, cooldown: 1000 }); const degraded = withLatency( async () => { throw new Error('down'); }, { p50: 500, p99: 3000, errorRate: 1.0 } ); for (let i = 0; i < 3; i++) { await cb.execute(() => degraded()).catch(() => {}); } expect(cb.state).toBe('open'); }); it('transitions to half-open after cooldown', async () => { jest.useFakeTimers(); const cb = new SimpleCircuitBreaker({ threshold: 1, cooldown: 5000 }); const failing = withLatency( async () => { throw new Error(); }, { p50: 100, p99: 500, errorRate: 1.0 } ); await cb.execute(() => failing()).catch(() => {}); expect(cb.state).toBe('open'); jest.advanceTimersByTime(5001); const healthy = withLatency(async () => 'ok', 'redis'); await cb.execute(() => healthy()); expect(cb.state).toBe('closed'); jest.useRealTimers(); }); });