circuit-breaker
v0.3.1@backendkit-labs/circuit-breaker
Prevent cascading failures with intelligent error classification.
Overview#
The circuit breaker pattern prevents cascading failures: when a downstream service starts failing repeatedly, the breaker opens — subsequent calls short-circuit immediately without hitting the service — then after a cooldown it enters a half-open state to probe recovery.
The critical differentiator is error classification. Traditional implementations (like opossum) treat all errors equally. In practice a card_declined from Stripe is a business error — the service worked correctly — while a connection timeout is an infrastructure failure. The isFailure callback lets you tell them apart: only infra failures count against the breaker, preventing phantom opens caused by expected business rejections.
Failure rate is measured over a configurable sliding window of recent calls (not a fixed time interval), giving a more accurate picture of current health. Built-in retry with exponential backoff + jitter is available for transient errors before the breaker is involved.
States & Lifecycle#
Normal operation. All calls go through. Failure rate is tracked in the sliding window.
Failure threshold exceeded. Calls return circuit_open immediately without hitting downstream.
Cooldown elapsed. One probe call is allowed. Success → Closed, failure → Open again.
Configuration#
| Prop | Type | Description |
|---|---|---|
| name | string | Unique identifier used in logs and metrics. |
| failureThreshold | number | Failure rate (0–100 %) that trips the breaker open. |
| sampleSize | number | Sliding window size — number of recent calls over which failure rate is measured. |
| cooldownMs | number | Milliseconds to stay OPEN before attempting a single half-open probe. |
| isFailure | (err) => boolean | Returns true for infrastructure errors that count against the breaker. Return false for business errors (4xx, domain rejections) to prevent phantom opens. |
| onStateChange | (prev, next, metrics) => void | Called on every state transition with a metrics snapshot. Use for alerting or logging. |
| retry.attempts | number | Number of retry attempts with exponential backoff + jitter before the breaker counts a failure. |
| retry.baseDelayMs | number | Initial retry delay in ms. Doubles each attempt. |
NestJS Integration#
Register the module once, then use the decorator on any service method. The CircuitBreakerRegistry provides pre-configured factories so you do not need to tune thresholds for common patterns.
import { CircuitBreakerModule } from '@backendkit-labs/circuit-breaker/nestjs'; @Module({ imports: [CircuitBreakerModule.forRoot()], }) export class AppModule {} // payment.service.ts @Injectable() export class PaymentService { @WithCircuitBreaker({ name: 'stripe', failureThreshold: 40, cooldownMs: 30_000 }) async charge(dto: ChargeDto): Promise<Result<PaymentIntent, ChargeError>> { return this.stripeGateway.createCharge(dto); // result.error.type === 'circuit_open' when the breaker is open } }
CircuitBreakerRegistry
Instead of tuning individual instances, the registry provides three pre-configured factory methods covering the most common scenarios:
| Prop | Type | Description |
|---|---|---|
| getForHttpExternal(name) | CircuitBreaker | For calls to external HTTP APIs. 8-call window, 10-call sample, 5 s timeout. |
| getForService(name) | CircuitBreaker | For internal microservice calls. 20-call window, 20-call sample, 10 s timeout. |
| getForDatabase(name) | CircuitBreaker | For database connections. 15-call window, 15-call sample, 3 s timeout. |
import { CircuitBreakerRegistry } from '@backendkit-labs/circuit-breaker'; const registry = new CircuitBreakerRegistry(); // Pre-tuned for external HTTP — no manual threshold guessing const stripeBreaker = registry.getForHttpExternal('stripe'); const dbBreaker = registry.getForDatabase('postgres'); const orderBreaker = registry.getForService('order-service');
Examples#
From basic to production-grade — copy and adapt.
import { CircuitBreaker } from '@backendkit-labs/circuit-breaker'; const cb = new CircuitBreaker({ name: 'stripe', failureThreshold: 40, // open after 40 % failure rate sampleSize: 20, // over the last 20 calls cooldownMs: 30_000, // wait 30 s before probing again }); const result = await cb.execute(() => stripe.charges.create(dto)); if (result.ok) { return result.value; } else if (result.error.type === 'circuit_open') { throw new ServiceUnavailableException('Payment service down'); }
⚖️ vs. Alternatives#
Comparing against opossum — the most popular and established circuit breaker library in the Node.js ecosystem.
| Feature | @backendkit-labs/circuit-breaker | opossum (v9) |
|---|---|---|
| Business error classification | ✅ isBusinessError() | ❌ All errors equal |
| Sliding-window tracking | ✅ Count-based | ✅ Count-based |
| Half-open probing | ✅ | ✅ |
| getMetrics() | ✅ failureRate, totalCalls | ✅ stats object |
| onStateChange hook | ✅ + metrics snapshot | ✅ events |
| Result<T,E> integration | ✅ Native | ❌ Throws / callbacks |
| NestJS decorator | ✅ @UseCircuitBreaker | ❌ Manual wiring |
| AbortController support | ❌ | ✅ |
| Runtime dependencies | 0 (core) | 0 |
| Weekly downloads | Growing | ~2.2M |
✅ Supported · ❌ Not supported · ⚠️ Partial / workaround needed. Download counts are approximate weekly npm averages.