How to Troubleshoot OmniRoute Circuit Breaker OPEN State: A Complete Guide
When an OmniRoute provider displays OPEN, the circuit breaker has short-circuited routing due to accumulated upstream failures; resolve it by checking the health API for failure counts, verifying that only provider-level 5xx errors trigger the breaker, and resetting via POST /api/resilience/reset or the programmatic reset() method.
When routing providers in the diegosouzapw/OmniRoute repository begin failing, the Provider Circuit Breaker automatically isolates them by transitioning to an OPEN state. This prevents cascading failures across your routing infrastructure, but requires immediate diagnosis to restore service. This guide explains the internal mechanics of the breaker, why providers get stuck in OPEN, and the exact steps to validate and recover from the state.
Where the Circuit Breaker Lives in OmniRoute
The circuit breaker implementation resides in [src/shared/utils/circuitBreaker.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/shared/utils/circuitBreaker.ts). This file defines the CircuitBreaker class, the state machine transitions, and the public getCircuitBreaker() factory function used throughout the routing layer.
Persistence is handled through [src/lib/db/domainState.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/db/domainState.ts), which saves state to the domain_circuit_breakers database table via saveCircuitBreakerState() and loadCircuitBreakerState(). The high-level architecture and default thresholds are documented in [docs/architecture/RESILIENCE_GUIDE.md](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/docs/architecture/RESILIENCE_GUIDE.md).
Why Providers Enter the OPEN State
The breaker transitions to OPEN when specific failure thresholds are breached. Understanding these triggers is essential for root-cause analysis.
Provider-Level Failure Accumulation
The breaker monitors for upstream 5xx errors (408, 500, 502, 503, 504) and increments an internal failureCount. When this count reaches the configured failureThreshold (default varies by provider type), the breaker transitions to OPEN.
Degradation Threshold
If the failure count exceeds degradationThreshold (approximately 60% of failureThreshold), the provider enters a DEGRADED state first. This logs warnings but still allows traffic. Only after crossing the full failureThreshold does it open completely.
Immediate-Open for Critical Failures
Certain FailureKind classifications (e.g., rate_limit) are configured with immediateOpen: true. These skip the DEGRADED phase and transition directly to OPEN.
Half-Open Probe Failures
After the resetTimeout expires, the breaker enters HALF_OPEN and allows probe requests. If these probes fail, the openCycleCount increments and the effective timeout increases via exponential backoff, extending the OPEN duration.
Step-by-Step Troubleshooting Guide
Follow this diagnostic sequence when a provider shows OPEN in OmniRoute.
1. Check Breaker Status via Health API
Query the monitoring endpoint to retrieve current state and metrics:
GET /api/monitoring/health
The response contains an array of breaker objects with these critical fields:
state– Current state (CLOSED,DEGRADED,HALF_OPEN, orOPEN)failureCount– Current count toward the thresholdlastFailureTime– Timestamp of the most recent errorretryAfterMs– Time remaining until automatic reset attemptopenCycleCount– Number of consecutive open/close cycles indicating chronic issues
2. Inspect Transition History
Use the programmatic API to examine recent state changes:
import { getAllCircuitBreakerStatuses } from '@/shared/utils/circuitBreaker';
const statuses = await getAllCircuitBreakerStatuses();
const brokenProvider = statuses.find(s => s.state === 'OPEN');
console.log(brokenProvider.transitionHistory);
Look for the most recent transition to OPEN and examine the reason field to identify the specific FailureKind that triggered the change.
3. Verify Error Classification Filters
Ensure only provider-level errors increment the failure count. The breaker accepts an isFailure option that defaults to () => true, but production routes in [open-sse/handlers/chatHelpers.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/open-sse/handlers/chatHelpers.ts) and [open-sse/handlers/chat.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/open-sse/handlers/chat.ts) wrap the breaker with custom logic.
Confirm that your implementation uses isLocalStreamLifecycleError() (defined in [open-sse/services/accountFallback.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/open-sse/services/accountFallback.ts)) to exclude client-side aborts and "controller is already closed" errors from the failure count. Otherwise, transient client disconnects can falsely trip the breaker.
4. Confirm Threshold Configurations
Default thresholds are defined in [open-sse/config/constants.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/open-sse/config/constants.ts) and vary by authentication type:
- OAuth providers:
failureThreshold: 8,resetTimeout: 60s - API-key providers:
failureThreshold: 12,resetTimeout: 30s
Access these via Dashboard → Settings → Resilience to verify they match your operational requirements.
5. Audit Error Classification Logic
Review the classifyError() function in [open-sse/services/accountFallback.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/open-sse/services/accountFallback.ts). Misclassification can cause the breaker to ignore critical 5xx errors (failing to open when it should) or treat benign errors as critical (opening prematurely). Check any custom classification logic added for new providers.
6. Distinguish From Connection Cooldowns
Individual connections maintain separate rateLimitedUntil timestamps in the provider_connections table. If the circuit breaker reports OPEN but connections show future rate-limit dates, the breaker state may be stale. Check the Connection Cooldown layer documented in the Resilience Guide to differentiate between provider-level and connection-level blocks.
7. Exclude Model-Level Lockouts
Ensure the failure originated from the provider rather than a specific model. Model-scoped quota errors should trigger the Model Lockout mechanism (detailed in the Resilience Guide) rather than the provider circuit breaker. Opening the entire provider for a single model's quota exhaustion reduces routing capacity unnecessarily.
8. Manually Reset the Breaker
If the breaker is stuck or you need immediate recovery, use the reset API:
POST /api/resilience/reset
Alternatively, programmatically reset via Node REPL or maintenance scripts:
import { getCircuitBreaker } from '@/shared/utils/circuitBreaker';
const breaker = getCircuitBreaker('openai');
breaker.reset(); // Clears failureCount, openCycleCount, and DB persistence
9. Validate Recovery
After resetting, send controlled test requests to the provider. Monitor the health API to confirm the state transitions back to CLOSED via the internal _onSuccess handler (lines 53-71 in the source). Verify that failureCount remains at zero and no new transitions to OPEN occur.
Programmatic Diagnostics and Recovery
Query All Breaker Statuses
import { getAllCircuitBreakerStatuses } from '@/shared/utils/circuitBreaker';
const statuses = await getAllCircuitBreakerStatuses();
statuses.forEach((s) => {
console.log(
`${s.name}: ${s.state} (failures=${s.failureCount}, retryIn=${s.retryAfterMs}ms)`
);
});
Custom Failure Filtering
Prevent client aborts from counting against the breaker:
import { getCircuitBreaker, isLocalStreamLifecycleError } from '@/shared/utils/circuitBreaker';
const breaker = getCircuitBreaker('anthropic', {
isFailure: (err) => {
// Only count actual provider failures, not local stream interruptions
return !isLocalStreamLifecycleError(err);
},
});
Force Reset a Specific Provider
import { getCircuitBreaker } from '@/shared/utils/circuitBreaker';
// Immediately clear OPEN state for a specific provider
const breaker = getCircuitBreaker('openai');
breaker.reset();
console.log('Breaker reset to CLOSED');
Summary
- Location: The circuit breaker logic lives in
src/shared/utils/circuitBreaker.ts, with persistence handled insrc/lib/db/domainState.ts. - Triggers: Providers enter
OPENafter upstream 5xx errors (408, 500, 502, 503, 504) exceedfailureThreshold, or immediately for criticalFailureKindtypes likerate_limit. - Diagnosis: Check
GET /api/monitoring/healthforfailureCountandopenCycleCount; inspecttransitionHistoryviagetAllCircuitBreakerStatuses(). - False Positives: Verify that
isFailurefilters exclude client aborts usingisLocalStreamLifecycleError()fromaccountFallback.ts. - Recovery: Reset via
POST /api/resilience/resetor programmaticgetCircuitBreaker(name).reset(), then validate success counts reset to zero.
Frequently Asked Questions
How can I check which specific error caused the circuit breaker to open?
Query the breaker status using getAllCircuitBreakerStatuses() from src/shared/utils/circuitBreaker.ts and examine the transitionHistory array. The most recent entry contains a reason field indicating the FailureKind (e.g., rate_limit, timeout) that triggered the transition to OPEN.
Why does my provider stay OPEN even after the upstream service recovers?
The breaker enters HALF_OPEN after resetTimeout expires, but if probe requests fail during this phase, it reopens immediately and increments openCycleCount, which extends the timeout via exponential backoff. Use the health API to check openCycleCount and manually reset the breaker if the upstream recovery is confirmed but the probes continue failing due to stale state.
What is the difference between DEGRADED and OPEN states?
DEGRADED occurs when failureCount exceeds approximately 60% of failureThreshold (the degradation threshold); traffic still flows but is logged for monitoring. OPEN occurs when failureThreshold is fully reached, at which point the routing engine short-circuits all requests to that provider, causing combo strategies to skip it entirely.
Can client-side request cancellations trigger the circuit breaker?
Only if the isFailure callback is misconfigured. The default () => true counts all errors, but production implementations in open-sse/handlers/chatHelpers.ts use isLocalStreamLifecycleError() to filter out client aborts and "controller is already closed" errors. Ensure your route handlers wrap the breaker with this filter to prevent false opens from client disconnects.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →