When an AI agent suddenly stops responding due to an OpenAI API outage or a local inference server crash, business processes grind to a halt. We've seen this many times: in production systems with 99.9% SLA, a 10-minute LLM downtime can mean losing thousands of orders. Recently, a fintech client's GPT-4o went down for 15 minutes due to a cloud failure—our fallback agents switched to Claude 3.5 in 400 ms, and the process never stalled. That's why we architect LLM and full-stack resilience that can take a hit: fallback providers, circuit breakers, checkpoints, and human escalation. All turnkey, with warranty and documentation.
How to make AI agents fault-tolerant?
The key is to ensure that when any component fails (LLM API, tool, orchestrator), the agent either instantly switches to a backup or degrades gracefully without losing progress. We implement four protection levels: LLM redundancy (fallback providers), circuit breaker for tools, checkpoint recovery for long-running tasks, and human escalation for unrecoverable errors. In practice, priority fallback with a circuit breaker is 2-3x more reliable than round-robin under peak load. AI agent SLA reaches 99.99% with this approach.
Why priority fallback with circuit breaker is more reliable?
The first and most common scenario is the primary model (GPT-4o) being unavailable due to rate limits or an outage. We use a fallback chain: OpenAI → Anthropic → local model (LLaMA 3 8B via vLLM). Switching happens in under 500 ms using a circuit breaker for each provider. Health checks let us detect failures early.
Let's compare popular fallback strategies:
| Strategy | Switch time | Reliability (p99) | Implementation complexity |
|---|---|---|---|
| Round-robin | <50 ms | 99.9% | low |
| Priority with circuit breaker | <500 ms | 99.99% | medium |
| Multi-head (all at once) | <100 ms | 99.95% | high |
We choose priority with circuit breaker: it offers the best balance of performance and resilience. MLOps fault tolerance for such systems requires monitoring and automatic recovery.
Practical implementation: code and configs
Let's examine key Python components.
Fallback LLM Provider
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
import anthropic
import openai
class ResilientLLMClient:
def __init__(self):
self.providers = [
{"name": "openai", "client": openai.AsyncOpenAI(), "model": "gpt-4o"},
{"name": "anthropic", "client": anthropic.AsyncAnthropic(), "model": "claude-3-5-sonnet-20241022"},
{"name": "local_vllm", "client": openai.AsyncOpenAI(base_url="http://gpu1:8000/v1"), "model": "llama-3-8b"},
]
self.circuit_breakers = {p["name"]: CircuitBreaker() for p in self.providers}
async def generate(self, messages: list, **kwargs) -> str:
last_error = None
for provider in self.providers:
cb = self.circuit_breakers[provider["name"]]
if cb.is_open():
continue # provider is disabled by circuit breaker
try:
result = await self._call_provider(provider, messages, **kwargs)
cb.record_success()
return result
except Exception as e:
cb.record_failure()
last_error = e
logger.warning(f"Provider {provider['name']} failed: {e}")
raise RuntimeError(f"All LLM providers failed. Last error: {last_error}")
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10),
retry=retry_if_exception_type((openai.RateLimitError, openai.APIConnectionError))
)
async def _call_provider(self, provider: dict, messages: list, **kwargs) -> str:
response = await provider["client"].chat.completions.create(
model=provider["model"],
messages=messages,
**kwargs
)
return response.choices[0].message.content
Circuit Breaker Pattern
Our circuit breaker follows the classic Circuit Breaker pattern.
from enum import Enum
import time
class CircuitState(Enum):
CLOSED = "closed" # normal operation
OPEN = "open" # provider disabled
HALF_OPEN = "half_open" # testing recovery
class CircuitBreaker:
def __init__(self, failure_threshold=5, reset_timeout=60):
self.failure_threshold = failure_threshold
self.reset_timeout = reset_timeout
self.failure_count = 0
self.last_failure_time = 0
self.state = CircuitState.CLOSED
def is_open(self) -> bool:
if self.state == CircuitState.OPEN:
if time.time() - self.last_failure_time > self.reset_timeout:
self.state = CircuitState.HALF_OPEN
return False
return True
return False
def record_failure(self):
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = CircuitState.OPEN
logger.error(f"Circuit breaker OPEN after {self.failure_count} failures")
def record_success(self):
if self.state == CircuitState.HALF_OPEN:
self.state = CircuitState.CLOSED
self.failure_count = 0
logger.info("Circuit breaker CLOSED - provider recovered")
Checkpoint and task recovery
For long-running tasks (>1 min), we save progress in Redis:
@dataclass
class AgentCheckpoint:
task_id: str
step_index: int
completed_steps: list[StepResult]
state: dict # arbitrary agent state
saved_at: datetime
class CheckpointManager:
def __init__(self, storage: Redis):
self.storage = storage
async def save(self, checkpoint: AgentCheckpoint):
key = f"checkpoint:{checkpoint.task_id}"
await self.storage.setex(
key,
3600, # 1 hour TTL
pickle.dumps(checkpoint)
)
async def load(self, task_id: str) -> AgentCheckpoint | None:
key = f"checkpoint:{task_id}"
data = await self.storage.get(key)
return pickle.loads(data) if data else None
async def resume_or_start(self, task: AgentTask) -> AgentCheckpoint:
existing = await self.load(task.id)
if existing:
logger.info(f"Resuming task {task.id} from step {existing.step_index}")
return existing
return AgentCheckpoint(task_id=task.id, step_index=0, completed_steps=[], state={}, saved_at=datetime.utcnow())
Human escalation for unrecoverable errors
class EscalationPolicy:
MAX_RETRIES = 3
MAX_FALLBACK_ATTEMPTS = 2
async def handle_failure(
self,
task: AgentTask,
error: Exception,
retry_count: int
) -> EscalationDecision:
if retry_count < self.MAX_RETRIES and self._is_retriable(error):
return EscalationDecision(action="retry", delay_seconds=2 ** retry_count)
if isinstance(error, ToolUnavailableError):
return EscalationDecision(action="use_fallback_tool", tool=self._get_fallback_tool(error.tool))
# Escalate to human
await self.notify_human(task, error, retry_count)
return EscalationDecision(
action="escalate",
message=f"Task {task.id} requires human intervention after {retry_count} retries: {error}"
)
Steps to implement fault tolerance
We follow a proven process:
- Analyze (1-2 days): audit current architecture, identify single points of failure, gather RTO/RPO requirements. For multi-agent redundancy, critical scenarios must be defined.
- Design (3-5 days): select patterns (retry, circuit breaker, checkpoint), prototype provider integrations.
- Implement (2-4 weeks): write code, configure circuit breakers, deploy checkpoint storage, set up escalation.
- Test (1 week): chaos testing—emulate LLM API failures, tool crashes, overloads. Measure p99 latency and successful recovery rate.
- Deploy and document (2-3 days): canary rollout, monitor all layers, write runbooks.
We also conduct load testing with simulated failures in each layer. This uncovers hidden issues like asymmetric timeouts between different providers.
Fault tolerance layer comparison
| Layer | Component | Pattern | Recovery time |
|---|---|---|---|
| LLM | Providers | Fallback + circuit breaker | <500 ms |
| Tools | API calls | Retry + exponential backoff | <10 s |
| Agent | Task logic | Checkpoint + resume | <100 ms |
| Orchestrator | Distribution | Health check + reassign | <1 s |
Timelines and what's included
Estimated timelines: 2 to 6 weeks depending on system complexity and number of providers. Pricing is determined after an audit.
Deliverables:
- Deployed fallback LLM client with circuit breaker
- Checkpoint system on Redis (or another storage)
- Human escalation policy integrated with a messenger
- Chaos testing scenario
- Documentation (runbook, architecture diagram)
- Team training (1 hour)
- 3-month warranty on implemented components
Example circuit breaker configuration for production
circuit_breaker:
failure_threshold: 5
reset_timeout: 60
half_open_max_requests: 1
monitor_interval: 10
Adjust settings based on provider p99 latency. For OpenAI with typical 2-3 second latency, use reset_timeout 60 seconds; for a local model, 10-15 seconds.
Common design mistakes
- Same timeout for all providers. Different LLM APIs have different p99 latency. Tune individual timeouts.
- Resetting circuit breaker on every success. After opening, make only one trial request per reset_timeout, otherwise the provider will be hammered with errors.
- No graceful degradation. If all providers fail, the agent should report it, not spin in an infinite loop.
Our decade of experience: we've built fault-tolerant AI agents for 20+ projects—from e-commerce chatbots to warehouse automation systems. Get a consultation on your architecture: contact us for a 1-day audit. Order fault tolerance, and your agents will stop fearing failures.







