Skip to main content

LLM Fallback Chains

Open In Colab

Production LLM services go down. Rate limits get hit. Network timeouts happen. A fallback chain ensures your application keeps responding even when your primary provider is unavailable, by automatically trying a sequence of providers in priority order.

What you'll build: A three-tier fallback chain (GPT-4o → Claude Haiku → Groq Llama) with per-provider circuit breakers, configurable retry logic, and transparent logging of which provider answered each request. Time: ~15 min. Difficulty: Intermediate

Prerequisites​

pip install synapsekit[openai,anthropic,groq]
export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-ant-...
export GROQ_API_KEY=gsk_...

What you'll learn​

  • Use fallback_chain() to declare provider priority order
  • Attach CircuitBreaker instances to each provider in the chain
  • Configure per-provider retry counts and timeouts
  • Handle AllProvidersFailedError when the entire chain is exhausted
  • Inspect FallbackResult.provider_used to track which provider answered

Step 1: Import and configure providers​

import asyncio
from synapsekit import LLMConfig
from synapsekit.llms.openai import OpenAILLM
from synapsekit.llms.anthropic import AnthropicLLM
from synapsekit.llms.groq import GroqLLM
from synapsekit.resilience import CircuitBreaker, fallback_chain, AllProvidersFailedError

# Use a shared config so all providers in the chain behave consistently.
# Fallback providers should use cheaper/faster models since they are the safety net.
primary_llm = OpenAILLM(
model="gpt-4o",
config=LLMConfig(temperature=0.2, max_tokens=1024)
)

secondary_llm = AnthropicLLM(
model="claude-haiku-3-5",
config=LLMConfig(temperature=0.2, max_tokens=1024)
)

tertiary_llm = GroqLLM(
model="llama-3.1-8b-instant", # Fastest and cheapest — last line of defence
config=LLMConfig(temperature=0.2, max_tokens=512)
)

Step 2: Attach circuit breakers to each provider​

# Each provider gets its own circuit breaker with settings tuned to that provider's
# SLA characteristics. Groq recovers quickly so its timeout is shorter.
openai_breaker = CircuitBreaker(
name="openai-gpt4o",
failure_threshold=3,
window_seconds=60,
recovery_timeout=60,
)

anthropic_breaker = CircuitBreaker(
name="anthropic-haiku",
failure_threshold=3,
window_seconds=60,
recovery_timeout=45,
)

groq_breaker = CircuitBreaker(
name="groq-llama",
failure_threshold=5,
window_seconds=60,
recovery_timeout=20,
)

Step 3: Build the fallback chain​

# fallback_chain() accepts a list of (llm, circuit_breaker) pairs in priority order.
# SynapseKit tries each in sequence: if the first provider raises an exception or
# its breaker is OPEN, it moves to the next, and so on.
chain = fallback_chain(
providers=[
(primary_llm, openai_breaker),
(secondary_llm, anthropic_breaker),
(tertiary_llm, groq_breaker),
],
# Optional: retry each provider this many times before moving to the next.
# Useful for transient network errors that aren't provider outages.
retries_per_provider=1,
)

Step 4: Generate with automatic failover​

async def generate_with_fallback(prompt: str) -> str:
"""Send a prompt through the fallback chain and return the first successful response."""
try:
result = await chain.agenerate(prompt)

# FallbackResult exposes which provider actually answered,
# so you can log, alert, or adjust routing dynamically.
if result.provider_used != "openai-gpt4o":
print(f" [fallback] Primary unavailable — answered by {result.provider_used}")

return result.text

except AllProvidersFailedError as e:
# All three providers failed — surface a user-friendly error
print(f" [fallback] All providers failed: {e.errors}")
return "Service temporarily unavailable. Please try again shortly."

Step 5: Simulate failures to observe fallback behaviour​

async def demo_fallback():
"""Show the chain falling through providers as each one is artificially failed."""
print("=== Fallback Chain Demo ===\n")

# Normal operation — primary handles the request
prompt = "What is idempotency in REST APIs?"
print(f"Prompt: {prompt!r}")
answer = await generate_with_fallback(prompt)
print(f"Answer: {answer[:100]}...\n")

# Simulate primary failure: trip the OpenAI breaker manually
for _ in range(openai_breaker.failure_threshold):
openai_breaker._record_failure() # Internal method for testing only

print("(OpenAI breaker now OPEN — next call will fall through to Anthropic)\n")
print(f"Prompt: {prompt!r}")
answer = await generate_with_fallback(prompt)
print(f"Answer: {answer[:100]}...\n")

# Simulate both primary and secondary failing
for _ in range(anthropic_breaker.failure_threshold):
anthropic_breaker._record_failure()

print("(Anthropic breaker now OPEN — next call will fall through to Groq)\n")
print(f"Prompt: {prompt!r}")
answer = await generate_with_fallback(prompt)
print(f"Answer: {answer[:100]}...\n")

Complete working example​

import asyncio
from synapsekit import LLMConfig
from synapsekit.llms.openai import OpenAILLM
from synapsekit.llms.anthropic import AnthropicLLM
from synapsekit.llms.groq import GroqLLM
from synapsekit.resilience import CircuitBreaker, fallback_chain, AllProvidersFailedError

async def main():
config = LLMConfig(temperature=0.2, max_tokens=512)

chain = fallback_chain(
providers=[
(
OpenAILLM(model="gpt-4o-mini", config=config),
CircuitBreaker("openai", failure_threshold=3, recovery_timeout=30),
),
(
AnthropicLLM(model="claude-haiku-3-5", config=config),
CircuitBreaker("anthropic", failure_threshold=3, recovery_timeout=30),
),
(
GroqLLM(model="llama-3.1-8b-instant", config=config),
CircuitBreaker("groq", failure_threshold=5, recovery_timeout=15),
),
],
retries_per_provider=1,
)

prompts = [
"Explain the difference between authentication and authorisation.",
"What is a deadlock and how do you prevent one?",
"Summarise the CAP theorem in three bullet points.",
]

for prompt in prompts:
print(f"\nPrompt: {prompt!r}")
try:
result = await chain.agenerate(prompt)
print(f"Provider: {result.provider_used}")
print(f"Answer: {result.text[:120]}...")
except AllProvidersFailedError:
print("All providers failed — check your API keys and network.")

asyncio.run(main())

Expected output​

Prompt: 'Explain the difference between authentication and authorisation.'
Provider: openai
Answer: Authentication verifies who you are — it answers the question "are you who you
claim to be?" using credentials such as passwords or tokens. Authorisation ...

Prompt: 'What is a deadlock and how do you prevent one?'
Provider: openai
Answer: A deadlock occurs when two or more processes are each waiting for a resource ...

Prompt: 'Summarise the CAP theorem in three bullet points.'
Provider: openai
Answer: • Consistency: Every read receives the most recent write or an error...

How it works​

fallback_chain() returns a FallbackChain object that wraps each (llm, breaker) pair. When agenerate() is called, it iterates the pairs in order. For each pair it first checks the breaker state: if OPEN it skips immediately to the next provider without making a network call. If CLOSED or HALF_OPEN it attempts generation, wrapping any exception as a breaker failure. After retries_per_provider failed attempts it moves on. If all pairs are exhausted it raises AllProvidersFailedError with a list of the individual errors from each provider.

Variations​

Streaming with fallback:

# FallbackChain.astream() streams from the first available provider
async for chunk in chain.astream("Tell me about neural networks"):
print(chunk.text, end="", flush=True)

Weighted random selection instead of strict priority:

from synapsekit.resilience import weighted_fallback_chain

chain = weighted_fallback_chain(
providers=[
(primary_llm, openai_breaker, weight=0.7),
(secondary_llm, anthropic_breaker, weight=0.3),
]
)

Per-request timeout:

import asyncio

async def generate_with_timeout(prompt: str, timeout: float = 5.0) -> str:
try:
result = await asyncio.wait_for(chain.agenerate(prompt), timeout=timeout)
return result.text
except asyncio.TimeoutError:
# Timeout counts as a failure for circuit breaker purposes
return "Request timed out."

Troubleshooting​

Chain always falls through to the tertiary provider Check that your API keys are correct and that you have not accidentally tripped all breakers in a previous test run. Each CircuitBreaker instance is stateful — create fresh instances per test.

AllProvidersFailedError in development This usually means all API keys are missing or invalid. Verify each key with a direct curl to the provider's health endpoint before testing the chain.

retries_per_provider causes slow fallover Each retry adds latency. For latency-sensitive applications, set retries_per_provider=0 to fall through immediately on any error.

Next steps​