Fallback and Circuit Breaker
Complete the full lesson to earn 25 points
Work through each section, then tap “Mark as Complete” on the last one.
✦ Skip the page breaks and see fewer ads — read each lesson on a single page with Pro
Module: Optimize GenAI Systems
Section: Scalability Patterns
Lesson: Fallback and Circuit Breaker Patterns in GenAI
Introduction: Why Reliability Matters in GenAI
When we build applications powered by Generative AI, we often treat the Large Language Model (LLM) as a black box that always provides a response. However, in production environments, LLM APIs are subject to the same failures as any other distributed system: rate limits, network timeouts, service outages, and model-specific errors. Unlike traditional software, where a failed database query might return a clear error code, a failed GenAI call often means a degraded user experience, a silent failure, or a complete system hang.
The "Fallback" and "Circuit Breaker" patterns are essential architectural strategies designed to handle these failures gracefully. A Fallback pattern ensures that if your primary model fails, the system has an alternative path—such as using a smaller, faster model, a cached response, or a hard-coded template—to keep the application functional. The Circuit Breaker pattern, on the other hand, prevents your system from repeatedly calling a failing service, which protects both your infrastructure from resource exhaustion and the provider from being overwhelmed by retries.
In this lesson, we will explore how to implement these patterns specifically for GenAI workloads. We will move beyond theory to look at how these patterns interact with prompt engineering, token costs, and latency requirements. By the end of this guide, you will understand how to build resilient AI systems that maintain high availability even when the underlying models encounter issues.
The Anatomy of Failure in GenAI Systems
Before diving into the patterns, it is vital to categorize why GenAI systems fail. Understanding the failure mode determines which pattern you should apply.
Common Failure Modes
- Rate Limiting (429 Too Many Requests): Your application has exceeded the tokens-per-minute (TPM) or requests-per-minute (RPM) quota allocated by the provider.
- Latency Spikes (Timeouts): The model is under high load and takes significantly longer than the expected response time, causing your upstream client to time out.
- Service Unavailability (503/504 Errors): The model provider's infrastructure is experiencing an outage.
- Model-Specific Errors (Content Filtering): The output is blocked by safety filters, or the model fails to return a valid structured format (e.g., JSON), rendering the response unusable.
- Degraded Performance: The model returns a response, but the quality is too poor to be useful for the end user.
Callout: Failure vs. Error It is important to distinguish between a technical error (like a 500 status code) and a functional failure (like a hallucinated or irrelevant response). While Circuit Breakers typically track technical errors, robust GenAI systems must also implement "Logic Fallbacks" that trigger when the content itself is invalid or fails a validation check.
Pattern 1: The Fallback Pattern
The Fallback pattern is a strategy where a system provides a default or alternative response when the primary operation fails. In the context of GenAI, this is often implemented as a tiered approach.
Tiered Fallback Strategies
- Model Tiering: If your primary model (e.g., GPT-4o or Claude 3.5 Sonnet) fails, you automatically switch to a smaller, more available model (e.g., GPT-4o-mini or Haiku).
- Provider Fallback: If your primary API provider (e.g., OpenAI) is down, you switch to a different provider (e.g., Anthropic or an open-source model hosted on your own infrastructure).
- Static/Cached Fallback: If all model calls fail, you return a pre-generated "safe" response or a cached response from a previous, similar query.
- Template Fallback: You return a structured, non-AI generated response that informs the user that the AI is currently unavailable, but provides the information in a static format.
Implementing a Fallback Mechanism
When implementing a fallback, you must ensure that the fallback mechanism does not introduce its own set of failures. The logic should be lightweight and highly reliable.
Example: Python Implementation of Model Fallback
import openai
def call_llm_with_fallback(prompt):
primary_model = "gpt-4o"
fallback_model = "gpt-4o-mini"
try:
# Attempt primary model
return call_openai(model=primary_model, prompt=prompt)
except (openai.APIError, openai.RateLimitError) as e:
print(f"Primary model failed: {e}. Switching to fallback.")
try:
# Attempt fallback model
return call_openai(model=fallback_model, prompt=prompt)
except Exception as e:
# Last resort: return a static error message
return "I am currently experiencing technical difficulties. Please try again later."
def call_openai(model, prompt):
# Standard API call logic here
pass
Note: When using a fallback model, remember that smaller models may have different prompt engineering requirements. You might need a slightly different system prompt for the smaller model to ensure the output remains consistent in structure and tone.
Pattern 2: The Circuit Breaker Pattern
The Circuit Breaker pattern prevents an application from repeatedly trying to execute an operation that is likely to fail. It acts as a switch:
- Closed State: Requests flow normally to the LLM.
- Open State: The breaker "trips" after a threshold of failures. Requests are blocked immediately, and a fallback is triggered without even attempting the LLM call.
- Half-Open State: After a timeout period, the system allows a limited number of test requests to see if the LLM provider has recovered.
Why Circuit Breakers are Critical for AI
In GenAI, requests can be extremely expensive and slow. If your system is failing, continuing to send requests to an unresponsive API provider is a waste of money and resources. Moreover, if your application is under high traffic, retrying failing requests can create a "retry storm," where your system compounds the provider's outage by flooding them with requests.
Implementing a Circuit Breaker
You can implement this using libraries like pybreaker in Python, or by building a simple state machine.
Step-by-Step Logic for a Circuit Breaker:
- Define Thresholds: Set the number of consecutive failures (e.g., 5 failures) that trip the circuit.
- Define Timeout: Set the duration (e.g., 60 seconds) for which the circuit remains "Open" before attempting a recovery.
- Monitor Status: Use a shared cache (like Redis) if your application is distributed across multiple containers or servers. This ensures the circuit state is consistent globally.
Example: Simplified Circuit Breaker
import time
class CircuitBreaker:
def __init__(self, failure_threshold=3, recovery_timeout=30):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.failures = 0
self.last_failure_time = None
self.state = "CLOSED"
def call(self, func, *args, **kwargs):
if self.state == "OPEN":
if time.time() - self.last_failure_time > self.recovery_timeout:
self.state = "HALF-OPEN"
else:
raise Exception("Circuit is OPEN. Request blocked.")
try:
result = func(*args, **kwargs)
self.reset()
return result
except Exception as e:
self.handle_failure()
raise e
def handle_failure(self):
self.failures += 1
self.last_failure_time = time.time()
if self.failures >= self.failure_threshold:
self.state = "OPEN"
def reset(self):
self.failures = 0
self.state = "CLOSED"
Comparison: Fallback vs. Circuit Breaker
| Feature | Fallback Pattern | Circuit Breaker Pattern |
|---|---|---|
| Primary Goal | Maintain functionality during failure | Prevent system resource exhaustion |
| Action Taken | Switches to an alternative source | Blocks calls to the failing source |
| Trigger | Any error or failure | Threshold of consecutive errors |
| Complexity | Low to Medium | Medium to High |
| Best Used For | Handling model-specific errors | Handling provider outages/timeouts |
Best Practices for GenAI Resilience
Implementing these patterns is only half the battle. To truly optimize your GenAI systems, you need to follow industry-standard best practices.
1. Implement Exponential Backoff with Jitter
When retrying a failed request, never retry immediately. Use exponential backoff (wait 1s, 2s, 4s, 8s, etc.). Crucially, add "jitter"—a random amount of time—to your wait periods. This prevents "thundering herd" problems where all your application instances retry at the exact same millisecond, crashing the provider's API again.
2. Contextual Fallbacks
Instead of a generic "error" fallback, provide context-aware fallbacks. If the user is asking for a summary of a document, your fallback could be an extractive summarization algorithm (a non-AI regex or keyword-based approach) rather than a smaller LLM. This provides a "good enough" answer rather than an error message.
3. Monitoring and Observability
You cannot fix what you do not measure. Track the following metrics specifically for your patterns:
- Circuit Breaker State Changes: Log every time the breaker opens or closes.
- Fallback Usage Rate: How often are you hitting your fallback models? If this is high, your primary model is unreliable.
- Latency per Model: Monitor the latency of your primary model vs. your fallback model to ensure the fallback doesn't cause a different bottleneck.
4. Input Validation
Before sending a request to the LLM, validate the input. If the input is empty, malformed, or contains sensitive data that violates your policy, do not call the LLM at all. This saves money and protects your system from "jailbreak" attempts or invalid requests.
Warning: The "Fallback Loop" Trap Be careful not to create a recursive failure loop. For example, if your fallback model also fails, ensure your code doesn't try to call the primary model again immediately. Always have a final, non-AI "catch-all" state that simply reports an error to the user.
Advanced Considerations: Distributed Systems
In a professional environment, your GenAI application likely runs on multiple nodes. A circuit breaker running in memory on one container will not know about failures happening on another container.
Using Redis for Shared State
To make your circuit breaker effective in a distributed system, store the failure count and the state of the breaker in a shared key-value store like Redis.
- Increment Failure: When a failure occurs, increment a counter in Redis with an expiration (TTL).
- Check State: Before every call, check the value of the key in Redis.
- Atomic Updates: Use atomic operations (like
INCRin Redis) to ensure that concurrent requests don't cause race conditions when updating the failure count.
Handling Token Limits
Rate limits are often specific to the model or the account. If you are using multiple API keys, implement a "Key Rotation" pattern. If one key hits a rate limit, the system should automatically switch to the next available key in your pool. This is a form of fallback that is highly effective for scaling GenAI workloads.
Common Pitfalls and How to Avoid Them
Pitfall 1: Over-Engineering the Fallback
Many developers try to create a "perfect" fallback that is just as good as the primary model. This is often impossible and expensive.
- Avoid: Spending weeks tuning a smaller model to match the quality of a larger one.
- Solution: Accept that the fallback is a "degraded" mode. Use it to provide a simple, helpful response, and inform the user that the service is running in limited capacity.
Pitfall 2: Ignoring Safety Filters
Sometimes, an LLM returns a "refusal" response due to safety filters. This is technically a "successful" API call (HTTP 200), but a "functional" failure.
- Avoid: Treating all HTTP 200 responses as valid data.
- Solution: Implement a response validator that checks for common refusal phrases (e.g., "I cannot fulfill this request") and triggers your fallback logic if detected.
Pitfall 3: Not Testing the "Open" State
How do you know your circuit breaker works if the provider is always up?
- Avoid: Assuming your error handling code is correct.
- Solution: Implement "Chaos Engineering" in your testing environment. Manually force the circuit breaker into an "Open" state to verify that your application correctly routes requests to the fallback path.
Step-by-Step Implementation Guide: Building a Resilient Pipeline
To put this all together, let’s design a resilient GenAI request pipeline.
Step 1: The Request Interceptor
Create a wrapper function that intercepts all LLM calls. This wrapper will be responsible for:
- Checking the circuit breaker state.
- Formatting the prompt.
- Executing the call.
Step 2: The Logic Controller
Inside the wrapper, implement the following flow:
- Check Circuit: Is the circuit open? If yes, jump to the fallback.
- Primary Attempt: Call the primary model.
- Validation: Did the model return a refusal? If yes, throw a custom exception.
- Success/Failure: If successful, return the result. If an exception occurs, increment the circuit failure counter.
- Fallback Trigger: If the primary fails, try the secondary model. If that fails, return the cached/static result.
Step 3: Global Configuration
Store your configuration (model names, thresholds, timeouts) in a central configuration file or environment variables. This allows you to tune your resilience settings without redeploying your entire application.
Code Example: A Full-Scale Resilience Pattern
This example demonstrates a more robust structure using the concepts discussed.
import time
import random
class GenAIResilienceManager:
def __init__(self):
self.circuit_open = False
self.failure_count = 0
self.threshold = 3
def get_response(self, prompt):
if self.circuit_open:
return self.get_fallback_response("Circuit is open, serving static content.")
try:
response = self.call_primary_llm(prompt)
self.failure_count = 0 # Success resets failures
return response
except Exception:
self.failure_count += 1
if self.failure_count >= self.threshold:
self.circuit_open = True
return self.get_fallback_response("Primary model failed, using secondary.")
def call_primary_llm(self, prompt):
# Simulate potential failure
if random.random() < 0.3:
raise Exception("API Down")
return "Primary Model Response"
def get_fallback_response(self, reason):
# In a real app, this might be a smaller model or a cached result
return f"Fallback Response: {reason}"
# Usage
manager = GenAIResilienceManager()
for i in range(10):
print(manager.get_response("Hello AI"))
Key Takeaways
- Failures are inevitable: In GenAI, you must design for failure from day one. Assume that your primary model provider will eventually experience latency or downtime.
- Fallback is about continuity: Use the Fallback pattern to ensure the user always gets an answer, even if it is a simplified or static one, rather than a blank screen or an error page.
- Circuit Breakers protect your infrastructure: Use this pattern to stop the flow of requests to a failing service, preventing resource waste and "retry storms."
- Validate content, not just status codes: A 200 OK status code does not mean the LLM provided a useful response. Always validate that the output meets your quality and safety standards.
- Use shared state for distributed systems: If your GenAI application runs on multiple servers, ensure your circuit breaker state is synchronized via a shared cache like Redis.
- Test your failure paths: Use chaos engineering to verify that your fallbacks and circuit breakers actually engage when things go wrong. Don't wait for a production outage to find out your error handling is broken.
- Keep it simple: Avoid over-complicating your fallback logic. A simple, reliable fallback is always better than a complex, fragile one that is prone to its own errors.
By integrating these patterns, you transition from building "AI experiments" to building "production-grade AI services." The difference between a hobby project and a professional application often lies in how gracefully the system handles the inevitable reality of distributed network failures. Start by implementing basic retries with exponential backoff, then move to circuit breakers as your traffic grows. Your users will appreciate the consistency, and your infrastructure costs will be much easier to manage.
Enjoying the courses?
Everything stays free. Pro shows fewer ads, doubles your daily points limit so you progress twice as fast, and lets you read each lesson on one page.
- ✓ Fewer advertisements
- ✓ 2× daily points limit
- ✓ Distraction-free lessons