usertrust

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) ─────────────┘
StateBehavior
ClosedRequests pass through normally. Failures are counted.
OpenAll requests are immediately rejected with CircuitOpenError. No calls reach the provider.
Half-OpenRequests 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

  1. Call allowRequest() before forwarding to the provider
  2. If the breaker is open and the timeout has not expired, CircuitOpenError is thrown
  3. If the timeout has expired, the breaker transitions to half-open and allows the request
  4. On success, call recordSuccess() -- in half-open, this counts toward the success threshold
  5. 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
  }
}
OptionTypeDefaultDescription
circuitBreaker.failureThresholdnumber5Consecutive failures before the breaker trips open
circuitBreaker.resetTimeoutnumber60000Milliseconds 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.