← Back to DevBytes

How to Implement Circuit Breakers for External LLM APIs

How to Implement Circuit Breakers for External LLM APIs

Large Language Model (LLM) APIs are powerful, but they are also fragile dependencies. Rate limits, transient network failures, provider outages, and slow token streaming can all turn a healthy application into a cascading failure. A circuit breaker is a resilience pattern that stops your system from repeatedly calling a failing service, giving it time to recover and protecting your own resources. In this tutorial, we'll explore what circuit breakers are, why they're essential for LLM integrations, and how to implement them in practice.

What Is a Circuit Breaker?

A circuit breaker wraps an external call and monitors its success and failure rates. Based on recent behavior, it switches between three states:

This is conceptually similar to an electrical circuit breaker: when too much current flows, the breaker trips to prevent damage. Once conditions stabilize, it can be reset.

Why Circuit Breakers Matter for LLM APIs

LLM APIs have unique characteristics that make circuit breakers especially valuable:

How to Use Circuit Breakers with LLM APIs

Let's walk through a practical implementation in Python. We'll build a circuit breaker from scratch first to understand the mechanics, then show how to use a production-grade library.

1. A Minimal Circuit Breaker from Scratch

The core idea is a state machine that tracks failures and decides whether to allow a call. Here's a simple implementation:

import time
import random
from enum import Enum
from functools import wraps

class State(Enum):
    CLOSED = "closed"
    OPEN = "open"
    HALF_OPEN = "half_open"

class CircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=30, half_open_max_calls=3):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.half_open_max_calls = half_open_max_calls
        self.state = State.CLOSED
        self.failure_count = 0
        self.success_count = 0
        self.last_failure_time = None
        self.half_open_calls = 0

    def _before_call(self):
        if self.state == State.OPEN:
            if time.time() - self.last_failure_time >= self.recovery_timeout:
                print("[CircuitBreaker] Transitioning OPEN -> HALF_OPEN")
                self.state = State.HALF_OPEN
                self.half_open_calls = 0
            else:
                raise CircuitBreakerOpenError("Circuit is OPEN; failing fast")

        if self.state == State.HALF_OPEN and self.half_open_calls >= self.half_open_max_calls:
            raise CircuitBreakerOpenError("Circuit is HALF_OPEN and at trial limit")

        if self.state == State.HALF_OPEN:
            self.half_open_calls += 1

    def _on_success(self):
        if self.state == State.HALF_OPEN:
            self.success_count += 1
            if self.success_count >= self.half_open_max_calls:
                print("[CircuitBreaker] Transitioning HALF_OPEN -> CLOSED")
                self._reset()
        else:
            self.failure_count = 0

    def _on_failure(self):
        self.last_failure_time = time.time()
        if self.state == State.HALF_OPEN:
            print("[CircuitBreaker] Transitioning HALF_OPEN -> OPEN")
            self.state = State.OPEN
            self.success_count = 0
        else:
            self.failure_count += 1
            if self.failure_count >= self.failure_threshold:
                print("[CircuitBreaker] Transitioning CLOSED -> OPEN")
                self.state = State.OPEN

    def _reset(self):
        self.state = State.CLOSED
        self.failure_count = 0
        self.success_count = 0

    def call(self, func, *args, **kwargs):
        self._before_call()
        try:
            result = func(*args, **kwargs)
            self._on_success()
            return result
        except Exception as e:
            self._on_failure()
            raise

class CircuitBreakerOpenError(Exception):
    pass

Now let's wrap an LLM call with this breaker:

import openai

breaker = CircuitBreaker(failure_threshold=5, recovery_timeout=60)

def call_llm(prompt: str, model: str = "gpt-4o") -> str:
    try:
        return breaker.call(
            openai.chat.completions.create,
            model=model,
            messages=[{"role": "user", "content": prompt}],
        )
    except CircuitBreakerOpenError:
        # Fallback: use a cached response, a cheaper model, or a queue
        return "Service temporarily unavailable. Please try again later."
    except Exception as e:
        print(f"LLM call failed: {e}")
        raise

# Simulate usage
for i in range(20):
    try:
        response = call_llm("Summarize the benefits of circuit breakers.")
        print(f"Call {i}: OK")
    except Exception as e:
        print(f"Call {i}: FAILED - {e}")

2. Handling LLM-Specific Errors

Not every exception should count as a failure. A 400 Bad Request caused by a malformed prompt is a client error, not a service problem. You should only trip the breaker on transient or server-side failures:

from openai import RateLimitError, APIConnectionError, APITimeoutError, InternalServerError

def is_tripable_error(exc: Exception) -> bool:
    return isinstance(exc, (
        RateLimitError,
        APIConnectionError,
        APITimeoutError,
        InternalServerError,
    ))

class LLMSpecificCircuitBreaker(CircuitBreaker):
    def call(self, func, *args, **kwargs):
        self._before_call()
        try:
            result = func(*args, **kwargs)
            self._on_success()
            return result
        except Exception as e:
            if is_tripable_error(e):
                self._on_failure()
            # Non-tripable errors (e.g., 400) don't affect circuit state
            raise

3. Using a Production-Grade Library: pybreaker

For production systems, prefer a battle-tested library. pybreaker is a popular Python circuit breaker that supports listeners, async usage, and custom storage backends:

pip install pybreaker openai
import pybreaker
import openai
from openai import RateLimitError, APIConnectionError, APITimeoutError, InternalServerError

# Define which exceptions trip the breaker
tripable_exceptions = (
    RateLimitError,
    APIConnectionError,
    APITimeoutError,
    InternalServerError,
)

# Create a breaker: trip after 5 consecutive failures, recover after 60s
llm_breaker = pybreaker.CircuitBreaker(
    fail_max=5,
    reset_timeout=60,
    exclude=lambda exc: not isinstance(exc, tripable_exceptions),
)

@llm_breaker
def call_llm(prompt: str, model: str = "gpt-4o") -> str:
    return openai.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
    )

# Register a listener for observability
class LoggingListener(pybreaker.CircuitBreakerListener):
    def state_change(self, cb, old, new):
        print(f"[Breaker] {old.name} -> {new.name}")

llm_breaker.add_listener(LoggingListener())

# Usage with fallback
def safe_call_llm(prompt: str) -> str:
    try:
        return call_llm(prompt)
    except pybreaker.CircuitBreakerError:
        return fallback_response(prompt)

def fallback_response(prompt: str) -> str:
    return "I'm unable to process your request right now. Please try again shortly."

4. Async Circuit Breakers for Streaming and Concurrent Calls

Modern LLM apps often use async clients and streaming. Here's how to combine pybreaker with the async OpenAI client:

import asyncio
import pybreaker
from openai import AsyncOpenAI
from openai import RateLimitError, APIConnectionError, APITimeoutError, InternalServerError

client = AsyncOpenAI()

async_breaker = pybreaker.CircuitBreaker(
    fail_max=5,
    reset_timeout=60,
    exclude=lambda exc: not isinstance(exc, (
        RateLimitError, APIConnectionError, APITimeoutError, InternalServerError
    )),
)

@async_breaker
async def call_llm_async(prompt: str, model: str = "gpt-4o") -> str:
    response = await client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
    )
    return response.choices[0].message.content

async def stream_llm_async(prompt: str, model: str = "gpt-4o"):
    """Stream tokens with circuit breaker protection on connection."""
    if async_breaker.current_state == pybreaker.CircuitState.OPEN:
        raise pybreaker.CircuitBreakerError("Circuit is open")
    try:
        stream = await client.chat.completions.create(
            model=model,
            messages=[{"role": "user", "content": prompt}],
            stream=True,
        )
        async for chunk in stream:
            if chunk.choices[0].delta.content:
                yield chunk.choices[0].delta.content
        async_breaker._inc_counter()
    except (RateLimitError, APIConnectionError, APITimeoutError, InternalServerError) as e:
        async_breaker._handle_error(e)
        raise

async def main():
    try:
        result = await call_llm_async("Explain quantum computing in one sentence.")
        print(result)
    except pybreaker.CircuitBreakerError:
        print("Circuit is open; using fallback.")

asyncio.run(main())

5. Per-Model and Per-Tenant Breakers

A single global breaker is rarely sufficient. Different models have different rate limits and reliability profiles. Use a registry keyed by model (and optionally tenant):

from dataclasses import dataclass, field
from typing import Dict
import pybreaker

@dataclass
class BreakerRegistry:
    breakers: Dict[str, pybreaker.CircuitBreaker] = field(default_factory=dict)

    def get(self, model: str) -> pybreaker.CircuitBreaker:
        if model not in self.breakers:
            self.breakers[model] = pybreaker.CircuitBreaker(
                fail_max=5,
                reset_timeout=60,
                name=f"llm-{model}",
            )
        return self.breakers[model]

registry = BreakerRegistry()

def call_llm_with_model(prompt: str, model: str) -> str:
    breaker = registry.get(model)
    try:
        return breaker.call(lambda: openai.chat.completions.create(
            model=model,
            messages=[{"role": "user", "content": prompt}],
        ))
    except pybreaker.CircuitBreakerError:
        # Try a fallback model if the primary is down
        if model != "gpt-4o-mini":
            return call_llm_with_model(prompt, "gpt-4o-mini")
        raise

Best Practices

Conclusion

Circuit breakers are a critical resilience pattern for any application that depends on external LLM APIs. By failing fast when a provider is struggling, you protect your application's resources, reduce wasted spend on doomed retries, and create opportunities for graceful degradation. Start by identifying which LLM calls are on your critical path, wrap them with a breaker keyed by model or tenant, classify errors carefully so only transient failures trip the circuit, and always pair the breaker with a meaningful fallback. Combined with retries, timeouts, and good observability, circuit breakers turn unpredictable third-party dependencies into manageable, self-healing components of your system.

— Ad —

Google AdSense will appear here after approval

← Back to all articles