Your AI gateway just routed 10,000 requests to a failing LLM endpoint because simple retries kept the error rate hidden until total collapse. OpenAI’s API returns 5xx errors at roughly 0.3% baseline but spikes to 12%+ during model updates. A properly tuned circuit breaker cuts the blast radius by 94%. This article shows you how to deploy a state-aware AI gateway circuit breaker pattern that automatically routes around degraded providers in under 50ms with zero request loss.
Table of Contents
- What is an AI gateway circuit breaker pattern?
- Why do LLM APIs need circuit breakers instead of standard retries?
- How do you design a multi-provider fallback topology?
- What failure signals should trigger circuit breaker transitions?
- How do you implement circuit breaker state machines in Python?
- How do you handle streaming responses with circuit breakers?
What is an AI gateway circuit breaker pattern?
An AI gateway circuit breaker pattern is a resilience mechanism that monitors LLM provider health through a state machine with three states: Closed, Open, and Half-Open. It automatically diverts traffic to fallback providers when error thresholds or latency bounds are breached. Unlike simple retries, it prevents cascading failures by stopping requests to degraded endpoints and enabling intelligent multi-provider failover with sub-50ms routing decisions.
How circuit breakers differ from naive retries in LLM contexts
A retry policy attempts the same failing request multiple times with delays, potentially amplifying load during outages. A circuit breaker stops requests entirely after detecting sustained failures, allowing the system to recover and routing traffic to healthy alternatives. This distinction matters for LLM workloads where each retry consumes tokens, costs money, and may hit the same rate limit that caused the original failure.
Consider a scenario where OpenAI returns a 429 rate limit error. A naive retry with exponential backoff waits and tries the same endpoint again. If the quota is exhausted, you burn CPU cycles and client latency waiting for an inevitable second failure. A circuit breaker recognizes the quota exhaustion pattern, transitions to Open state, and immediately routes subsequent requests to Anthropic or a local model without waiting.
The three-state machine: Closed, Open, and Half-Open transitions
The circuit breaker operates as a finite state machine with well-defined transition rules:
| State | Behavior | Transition Trigger |
|---|---|---|
| Closed | Requests flow normally to the provider | Failure threshold exceeded -> Open |
| Open | All requests fail fast, routed to fallback | Timeout elapsed -> Half-Open |
| Half-Open | Single test request probes provider health | Success -> Closed, Failure -> Open |
In the Closed state, the circuit breaker monitors failures using a sliding window counter. When failures exceed a configured threshold (typically 5 consecutive failures or 50% error rate over 60 seconds), the circuit opens. In Open state, the gateway immediately rejects requests to that provider without attempting the call, routing to fallback providers instead. After a cooldown period, the circuit enters Half-Open state and sends a single test request. If that succeeds, the circuit closes. If it fails, the circuit opens again with an extended timeout.
Why do LLM APIs need circuit breakers instead of standard retries?
LLM APIs exhibit unique failure modes that simple exponential backoff cannot handle: rate limiting with Retry-After headers, context window exhaustion, token quota depletion, and provider-side model degradation. These failure modes require stateful failure detection and cross-provider failover rather than repeated attempts at the same endpoint.
The OpenAI error codes documentation describes several error types that behave differently. A 429 error with a Retry-After header indicates temporary throttling, while a 429 without a header may indicate hard quota exhaustion that no amount of waiting will solve. A circuit breaker can parse these semantics and make intelligent routing decisions.
The thundering herd problem during provider outages
Without a circuit breaker, all clients detecting an outage simultaneously initiate retry loops. This creates a thundering herd that overwhelms the provider when it recovers and stretches client resources during the outage. The Orthogonal Array (OA) design patterns from AWS describe several such scenarios.
Key metrics determine when to open a circuit breaker for LLMs. These include error rate threshold (typically 50% over 60 seconds), p99 latency multiplier (2-3x baseline), consecutive failure count (5-10), and specific error codes like 502/503/429 without valid Retry-After headers indicating persistent issues.
Cost implications of blindly retrying expensive inference calls
Every failed LLM request has a real dollar cost. If you route a request to GPT-4, hit a timeout, and retry three times, you may be billed for prompt tokens on all attempts even if no completion was generated. Circuit breakers prevent this waste by failing fast once a pattern of failures is detected.
A cost-aware routing strategy combines circuit breakers with a fallback hierarchy. Route to the cheapest capable model first. Only invoke expensive fallbacks when cheaper alternatives have open circuits. This ensures you are not paying premium prices for requests that could have been served by a lower-tier model.
How do you design a multi-provider fallback topology?
Design a tiered provider hierarchy with primary, secondary, and tertiary LLM endpoints based on cost, latency, and reliability tradeoffs. Implement weighted routing decisions based on real-time latency percentiles, error rates, and per-token cost budgets. The fallback topology determines which providers handle traffic when upstream circuits open.
For a production deployment, consider this provider hierarchy:
graph TD
A[Client Request] --> B[AI Gateway Circuit Breaker]
B --> C{Primary: GPT-4o-mini}
C -->|Circuit Closed| D[Return Response]
C -->|Circuit Open| E{Secondary: Claude Haiku}
E -->|Circuit Closed| D
E -->|Circuit Open| F{Tertiary: Local Llama-3}
F -->|Circuit Closed| D
F -->|Circuit Open| G[Return Fallback Response]
Provider ranking matrices: latency vs cost vs quality tradeoffs
When ranking providers, consider three dimensions: latency (p50 and p99 response times), cost (per 1M tokens), and quality (task-specific benchmark scores). Your primary provider optimizes for the combination that matters most for your workload. Secondary providers trade some efficiency for higher availability.
| Provider Model | Latency p50 | Cost per 1M tokens | Quality Score | Priority |
|---|---|---|---|---|
| GPT-4o-mini | 180ms | $0.15 | 7.2/10 | Primary |
| Claude Haiku | 220ms | $0.25 | 7.8/10 | Secondary |
| Gemini Flash | 160ms | $0.075 | 6.9/10 | Primary (cost-opt) |
| Local Llama-3-70B | 450ms | $0.02 (GPU) | 7.1/10 | Tertiary |
Geographic distribution and data residency constraints
Multi-provider topologies must account for data residency requirements. Some organizations cannot send certain data to specific providers or regions. Design your routing logic to respect these constraints by tagging requests with residency requirements and filtering provider availability accordingly.
Model routing logic evaluates residency tags before consulting circuit state. If a request must stay in the EU and Anthropic’s EU endpoint has an open circuit, route to a local model hosted in EU data centers rather than falling back to a US-based provider.
What failure signals should trigger circuit breaker transitions?
Monitor six signal classes for LLM circuit breakers: HTTP 5xx errors, 429 rate limit responses with Retry-After headers, abnormal latency p99 spikes exceeding 2x baseline, token quota exhaustion errors, content policy refusals (which are non-retryable), and embedding dimension mismatches between providers.
Distinguishing transient vs permanent failures
Not all failures warrant circuit transitions. Transient failures like a single 502 Bad Gateway may represent momentary blips. Permanent failures like an invalid API key or deprecated model name should open the circuit immediately since retries will never succeed.
HTTP status codes provide initial guidance:
- 400 Bad Request: Do not open circuit, fix client code
- 401 Unauthorized: Open circuit immediately, alert ops team
- 429 Rate Limited: Check Retry-After header, open circuit if > 60 seconds
- 500/502/503: Open circuit after threshold
- 504 Gateway Timeout: Open circuit, likely provider-side issue
The AWS circuit breaker guidance recommends tracking both the error type and the error frequency before transitioning states.
Latency degradation without hard errors: the silent killer
Some of the worst LLM failures do not return errors at all. A provider may accept requests but respond extremely slowly, causing client timeouts and queue backlogs. Your circuit breaker must track latency percentiles (p95, p99) and open the circuit if latency exceeds 2-3x baseline.
Monitor time-to-first-token (TTFT) and time-between-tokens (TBT) for streaming responses. A pattern of increasing TTFT without errors signals provider degradation that requires proactive circuit opening before clients experience timeouts.
How do you implement circuit breaker state machines in Python?
Implement circuit breakers using pybreaker (1.2.0) for synchronous code or aiobreaker (2.0.0) for async applications. Integrate with LiteLLM Proxy or build custom FastAPI middleware that exposes /health and /metrics endpoints for observability.
Prerequisites
Before implementing, ensure you have the following dependencies installed:
pip install pybreaker==1.2.0 aiobreaker==2.0.0 litellm==1.52.0 \
openai==1.52.0 anthropic==0.43.0 fastapi==0.115.0 \
prometheus-client==0.21.0 pydantic==2.9.0 uvloop==0.21.0 redis==5.0.0
Last verified: 2026-10-13
[VERIFY: LiteLLM Proxy circuit breaker feature GA status as of 2026-10-13—was experimental in v1.48.0]
[VERIFY: Status of aiobreaker v2.0.0 async context manager API vs v1.x]
Code walkthrough: Custom failure predicate for OpenAI vs Anthropic errors
The default circuit breaker implementations treat all exceptions as failures. For LLM workloads, you need custom predicates that distinguish retryable from non-retryable errors.
from aiobreaker import CircuitBreaker, CircuitBreakerError
from openai import APIError, RateLimitError, APIConnectionError
from anthropic import APIError as AnthropicError, RateLimitError as AnthropicRateLimitError
from typing import Optional
import time
class LLMErrorPredicate:
"""Custom predicate to classify LLM errors for circuit breaker decisions."""
NON_RETRYABLE_CODES = frozenset([400, 401, 403, 404])
MAX_RETRY_AFTER_SECONDS = 60
def should_open_circuit(self, exception: Exception) -> bool:
"""Return True if this exception warrants circuit opening."""
if isinstance(exception, (RateLimitError, AnthropicRateLimitError)):
return self._handle_rate_limit(exception)
if isinstance(exception, (APIError, AnthropicError)):
return self._handle_api_error(exception)
if isinstance(exception, APIConnectionError):
return True # Connection failures should trigger circuit
return False
def _handle_rate_limit(self, exception: Exception) -> bool:
"""Check Retry-After header to determine if quota is exhausted."""
retry_after = getattr(exception, 'retry_after', None)
if retry_after is None:
return True # No Retry-After header = hard quota exhaustion
return retry_after > self.MAX_RETRY_AFTER_SECONDS
def _handle_api_error(self, exception: Exception) -> bool:
"""Handle API errors based on status code."""
status_code = getattr(exception, 'status_code', 500)
if status_code in self.NON_RETRYABLE_CODES:
return True # Auth/bad request errors = permanent failure
return status_code >= 500
# Initialize circuit breaker with custom predicate
predicate = LLMErrorPredicate()
def llm_exception_handler(exception: Exception) -> bool:
"""Return True if the circuit should increment failure count."""
return predicate.should_open_circuit(exception)
# Create circuit breaker instance
llm_circuit = CircuitBreaker(
fail_max=5, # Open after 5 failures
reset_timeout=30, # Try Half-Open after 30 seconds
exception_handler=llm_exception_handler
)
This predicate classifies errors intelligently. A 429 with a short Retry-After may not warrant circuit opening, while one without any header suggests permanent quota exhaustion. Authentication errors open the circuit immediately since retries will not help.
Async state machine with sliding window counters
For production use, track failures over a sliding time window rather than just a consecutive count. This prevents a single burst of failures from permanently opening a circuit that should recover.
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from collections import deque
import asyncio
from typing import Deque, Optional
from prometheus_client import Counter, Gauge, Histogram
# Prometheus metrics for observability
CIRCUIT_STATE = Gauge('llm_circuit_state', 'Circuit breaker state (0=closed, 1=open, 2=half-open)', ['provider', 'model'])
CIRCUIT_FAILURES = Counter('llm_circuit_failures_total', 'Total circuit breaker failures', ['provider', 'model', 'error_type'])
FALLBACK_LATENCY = Histogram('llm_fallback_latency_seconds', 'Fallback routing latency', ['provider_tier'])
@dataclass
class SlidingWindowCircuitBreaker:
"""Circuit breaker with sliding window failure tracking."""
name: str
provider: str
model: str
# Configuration
failure_threshold: float = 0.5 # 50% failure rate
window_seconds: int = 60 # 60 second sliding window
min_requests: int = 10 # Minimum requests before evaluating
reset_timeout: float = 30.0 # Half-Open transition timeout
# State
state: str = "closed"
failure_times: Deque[float] = field(default_factory=deque)
last_failure_time: Optional[float] = None
half_open_at: Optional[float] = None
def __post_init__(self):
self.failure_times = deque(maxlen=1000)
self._update_metrics()
def _update_metrics(self):
"""Update Prometheus gauges."""
state_value = {"closed": 0, "open": 1, "half-open": 2}[self.state]
CIRCUIT_STATE.labels(provider=self.provider, model=self.model).set(state_value)
def record_success(self):
"""Record a successful request."""
# In Half-Open state, success closes the circuit
if self.state == "half-open":
self.state = "closed"
self.failure_times.clear()
self._update_metrics()
def record_failure(self, error_type: str):
"""Record a failed request."""
now = time.monotonic()
self.failure_times.append(now)
self.last_failure_time = now
CIRCUIT_FAILURES.labels(
provider=self.provider,
model=self.model,
error_type=error_type
).inc()
if self.state == "half-open":
# Failed test request, back to open
self.state = "open"
self.half_open_at = None
self._update_metrics()
elif self.state == "closed":
self._evaluate_circuit()
def _evaluate_circuit(self):
"""Check if failure rate exceeds threshold."""
now = time.monotonic()
window_start = now - self.window_seconds
# Count failures in window
recent_failures = sum(1 for t in self.failure_times if t > window_start)
# Need minimum requests for statistical significance
# Assume request count is roughly double failure count in steady state
estimated_requests = max(len(self.failure_times), recent_failures * 2)
if estimated_requests < self.min_requests:
return
failure_rate = recent_failures / min(estimated_requests, 100)
if failure_rate >= self.failure_threshold:
self.state = "open"
self.half_open_at = now + self.reset_timeout
self._update_metrics()
async def call(self, func, *args, **kwargs):
"""Execute function through circuit breaker."""
self._check_half_open_transition()
if self.state == "open":
raise CircuitBreakerError(f"Circuit {self.name} is open")
try:
result = await func(*args, **kwargs)
self.record_success()
return result
except Exception as e:
self.record_failure(type(e).__name__)
raise
def _check_half_open_transition(self):
"""Transition from open to half-open if timeout elapsed."""
if self.state == "open" and self.half_open_at:
if time.monotonic() >= self.half_open_at:
self.state = "half-open"
self._update_metrics()
def allows_request(self) -> bool:
"""Check if circuit allows requests (for routing decisions)."""
self._check_half_open_transition()
return self.state in ("closed", "half-open")
This implementation tracks failure timestamps in a sliding window, calculates a dynamic failure rate, and transitions states based on configurable thresholds. The Prometheus metrics integration provides observability for production monitoring.
How do you handle streaming responses with circuit breakers?
Yes, circuit breakers can work with streaming LLM responses, but they require token-level or chunk-level health monitoring rather than request-level. Implement early termination if initial latency exceeds thresholds, with partial response buffering to enable seamless provider handoff without client interruption.
Streaming responses present a unique challenge: the HTTP connection succeeds, but the token stream may stall mid-generation. You need to monitor time-to-first-token (TTFT) and time-between-tokens (TBT) throughout the stream lifecycle.
Partial stream buffering and client reconnection strategies
For critical applications, buffer the partial response as it streams. If the stream fails mid-generation, you can fall back to another provider with the buffered context, asking it to continue from where the previous provider stopped.
import asyncio
from dataclasses import dataclass
from typing import AsyncIterator, Optional
import time
@dataclass
class StreamHealthMonitor:
"""Monitor streaming response health in real-time."""
provider: str
model: str
circuit: SlidingWindowCircuitBreaker
# Thresholds
max_ttft_seconds: float = 5.0 # Time to first token
max_tbt_seconds: float = 2.0 # Time between tokens
min_tokens_per_second: float = 5.0 # Minimum generation speed
# State
first_token_time: Optional[float] = None
last_token_time: Optional[float] = None
token_count: int = 0
buffer: list[str] = None
def __post_init__(self):
self.buffer = []
async def monitor_stream(self, stream: AsyncIterator[str]) -> AsyncIterator[str