Circuit Breaker
Per-provider failure isolation with configurable thresholds and automatic recovery.
usertrust includes a per-provider circuit breaker that prevents cascading failures when an LLM provider is experiencing issues. If a provider fails repeatedly, the breaker trips open and fast-fails subsequent requests until the provider recovers.
Three States
The circuit breaker follows the standard state machine:
CLOSED → (failures hit threshold) → OPEN → (timeout expires) → HALF-OPEN → (successes) → CLOSED
↑ │
└──── (failure) ─────────────┘| State | Behavior |
|---|---|
| Closed | Requests pass through normally. Failures are counted. |
| Open | All requests are immediately rejected with CircuitOpenError. No calls reach the provider. |
| Half-Open | Requests are allowed through as probes (there is no cap on how many). Each success counts toward halfOpenSuccessThreshold; a single failure returns to open. |
CircuitBreaker
const breaker = new CircuitBreaker("anthropic", {
failureThreshold: 5, // trips after 5 consecutive failures
resetTimeoutMs: 60_000, // try again after 60 seconds
halfOpenSuccessThreshold: 2, // 2 successes to fully close
});
breaker.allowRequest(); // throws CircuitOpenError if open
breaker.recordSuccess(); // closed: resets the failure count to 0; half-open: counts toward closing
breaker.recordFailure(); // increments toward open
breaker.reset(); // force close
breaker.getState(); // "closed" | "open" | "half-open" — applies the open → half-open timeout
breaker.snapshot(); // CircuitBreakerSnapshot (see below)Request Flow
- Call
allowRequest()before forwarding to the provider - If the breaker is open and the timeout has not expired,
CircuitOpenErroris thrown - If the timeout has expired, the breaker transitions to half-open and allows the request
- On success, call
recordSuccess()-- in half-open, this counts toward the success threshold - On failure, call
recordFailure()-- in half-open, this immediately returns to open
CircuitBreakerRegistry
The registry manages breakers for multiple providers. Each provider key gets its own independent breaker instance.
const registry = new CircuitBreakerRegistry({
failureThreshold: 5,
resetTimeoutMs: 60_000,
});
// Get or create a breaker for a provider
const breaker = registry.get("anthropic");
// Snapshot all breakers
const all = registry.allSnapshots();
// { anthropic: { state: "closed", ... }, openai: { state: "open", ... } }
// Reset all breakers
registry.resetAll();
// Number of tracked breakers
const count = registry.size;CircuitBreakerSnapshot
The snapshot provides a read-only view of a breaker's current state:
interface CircuitBreakerSnapshot {
readonly state: "closed" | "open" | "half-open";
readonly consecutiveFailures: number;
readonly halfOpenSuccesses: number;
readonly lastFailureTime: number | null;
readonly lastStateChange: number;
}CircuitOpenError
When a request is rejected by an open breaker, a CircuitOpenError is thrown. It extends Error and includes the breaker key for identification:
try {
breaker.allowRequest();
} catch (err) {
if (err instanceof CircuitOpenError) {
console.log(err.breakerKey); // "anthropic"
// Fall back to a different provider or queue the request
}
}Configuration
Set circuit breaker thresholds in usertrust.config.json:
{
"circuitBreaker": {
"failureThreshold": 5,
"resetTimeout": 60000
}
}| Option | Type | Default | Description |
|---|---|---|---|
circuitBreaker.failureThreshold | number | 5 | Consecutive failures before the breaker trips open |
circuitBreaker.resetTimeout | number | 60000 | Milliseconds after the last recorded failure before an open breaker transitions to half-open |
Both values must be integers; a non-integer fails config validation. These are the only two circuit-breaker keys the schema reads — any other key under circuitBreaker, including halfOpenSuccessThreshold, is dropped silently rather than rejected. The registries that trust() and createGovernor() build pass only failureThreshold and resetTimeout (as resetTimeoutMs), so halfOpenSuccessThreshold stays at its default of 2 for governed clients and cannot be changed from config or options. The constructor option only applies to a CircuitBreaker or CircuitBreakerRegistry you construct and drive yourself.