Circuit Breaker
CircuitBreaker is a class from @omnitron-dev/titan/utils. There is
no @CircuitBreaker decorator — instantiate the class and use
.execute() around the work you want to protect.
States
enum CircuitState {
Closed = 'closed',
Open = 'open',
HalfOpen = 'half-open',
}
| State | Behaviour |
|---|---|
| Closed | Calls pass through. Failures count toward the threshold. |
| Open | Calls fail immediately. No backend hit. |
| HalfOpen | One probe call goes through. Success → Closed. Failure → Open. |
Transitions:
Basic usage
import { CircuitBreaker, CircuitState } from '@omnitron-dev/titan/utils';
const breaker = new CircuitBreaker({
failureThreshold: 5, // 5 failures opens the circuit
resetTimeout: 60_000, // ms in Open before allowing a probe
// …additional knobs in CircuitBreakerConfig
});
// Use it to wrap calls.
try {
const result = await breaker.execute(() => callStripe(req));
} catch (e) {
// If the circuit is open, .execute throws immediately.
}
The full CircuitBreakerConfig shape lives in
utils/resilience.ts. Common knobs (consult the source for the
canonical list):
failureThreshold— failures before opening (default5).resetTimeout— ms inOpenbefore transitioning toHalfOpen(default30000).successThreshold— successful probes inHalfOpenbefore returning toClosed(default1).volumeThreshold— minimum number of calls before the failure threshold takes effect (default10; avoids tripping on a single bad sample).failureRateThreshold— open when the failure rate (0–100) over the sliding window exceeds this, instead of an absolute count.requestTimeout— optional per-call timeout (ms) applied insideexecute().
Per-instance state
Each CircuitBreaker instance has its own state. Use one instance
per dependency you want to protect:
@Service({ name: 'Payments' })
class PaymentsService {
private readonly stripeBreaker = new CircuitBreaker({ failureThreshold: 5, resetTimeout: 60_000 });
private readonly paypalBreaker = new CircuitBreaker({ failureThreshold: 3, resetTimeout: 30_000 });
@Public()
async chargeStripe(req: ChargeRequest) {
return this.stripeBreaker.execute(() => this.stripe.charge(req));
}
@Public()
async chargePaypal(req: ChargeRequest) {
return this.paypalBreaker.execute(() => this.paypal.charge(req));
}
}
A failure in chargeStripe opens only the Stripe circuit;
PayPal stays available.
Observability
CircuitBreaker extends EventEmitter. The emitted events are
stateChange, success, failure, rejected, and reset — there
are no separate open / close / halfOpen events. Watch
stateChange and branch on the new state:
breaker.on('stateChange', ({ previousState, newState }) => {
if (newState === CircuitState.Open) metrics.counter('breaker.open').inc();
if (newState === CircuitState.Closed) metrics.counter('breaker.close').inc();
if (newState === CircuitState.HalfOpen) log.info('probing recovery');
});
breaker.on('rejected', () => metrics.counter('breaker.rejected').inc());
Reading state
breaker.getState() // CircuitState (state is private — use the getter)
breaker.getMetrics() // CircuitBreakerMetrics — total calls, failures, etc.
For imperative gating without execute(), the breaker also exposes
isOpen(), recordSuccess(), and recordFailure().
Composing with retry
import { CircuitBreaker, retry, isOperationalError } from '@omnitron-dev/titan/utils';
const breaker = new CircuitBreaker({ failureThreshold: 5, resetTimeout: 60_000 });
await breaker.execute(() =>
retry(() => callBackend(), { maxRetries: 3, shouldRetry: isOperationalError }),
);
Breaker outside, retry inside. When the circuit is open, retries
don't run — the breaker rejects before retry() is called.
Anti-patterns
- Threshold too low. A breaker that opens on the first failure
flaps constantly. Pair
failureThresholdwithvolumeThreshold(or per-window counts). timeouttoo short. The dependency hasn't recovered; the probe fails; the circuit re-opens. Real recovery takes seconds to minutes.- Same breaker for unrelated dependencies. A failure in one service opens the circuit for another. Use one breaker per dependency.
- No fallback. Every breaker should have a "what do we do when the circuit is open" plan. Cached value? Default? Error to the user? Decide explicitly.
→ Next: Timeout.