n4nAI

Multi-provider failover: a tutorial for production agents

Hands-on multi-provider failover tutorial for production agents: build resilient LLM routing with automatic fallback across OpenAI-compatible endpoints.

n4n Team3 min read638 words

Audio narration

Coming soon — every post will get a voice note here.

Building resilient agentic systems means planning for provider outages before they happen. This multi-provider failover tutorial shows you how to wire automatic fallback into a production LLM agent using nothing but standard OpenAI-compatible calls and a thin client wrapper. You will leave with runnable code that degrades gracefully when a model vendor throws 429s or drops dead.

Prerequisites

  • Python 3.11 or newer
  • openai Python SDK >= 1.40 (provides OpenAI, RateLimitError, APIError)
  • tenacity if you want retry decorators (optional for this pattern)
  • API keys for at least two OpenAI-compatible providers. I’ll use OpenAI and Groq, but any endpoint that speaks the /v1/chat/completions shape works.
  • Environment variables: OPENAI_API_KEY, GROQ_API_KEY.
pip install openai tenacity
export OPENAI_API_KEY=sk-...
export GROQ_API_KEY=gsk-...

Step 1: Declare your provider chain

Order matters. Put the provider with the best latency or quality first, and the cheap backup last. The wrapper will try them in sequence and stop at the first success.

import os

PROVIDERS = [
    {
        "name": "openai",
        "base_url": "https://api.openai.com/v1",
        "api_key": os.environ["OPENAI_API_KEY"],
        "model": "gpt-4o-mini",
    },
    {
        "name": "groq",
        "base_url": "https://api.groq.com/openai/v1",
        "api_key": os.environ["GROQ_API_KEY"],
        "model": "llama-3.1-8b-instant",
    },
]

Step 2: Implement the failover client

We instantiate one OpenAI client per provider. On RateLimitError or APIError we rotate to the next. Authentication or validation errors bubble up—those are config bugs, not transient failures.

from openai import OpenAI, APIError, RateLimitError

class FailoverChat:
    def __init__(self, providers):
        self.clients = [
            (p, OpenAI(base_url=p["base_url"], api_key=p["api_key"]))
            for p in providers
        ]

    def complete(self, messages, **kwargs):
        last_err = None
        for p, client in self.clients:
            try:
                resp = client.chat.completions.create(
                    model=p["model"], messages=messages, **kwargs
                )
                return {"provider": p["name"], "response": resp}
            except (RateLimitError, APIError) as e:
                last_err = e
                continue
        raise RuntimeError(f"All providers failed: {last_err}")

Instantiate and run a smoke test:

chat = FailoverChat(PROVIDERS)
out = chat.complete([{"role": "user", "content": "Ping"}])
print(out["provider"], out["response"].choices[0].message.content)

Expected output (truncated):

openai Pong! How can I help you today?

If OpenAI is returning 429s in your region, you’ll see groq printed without changing a line of calling code.

Streaming caveat

Streaming complicates failover: you cannot switch providers mid-token. Either buffer the full response before yielding to the agent, or restart the stream on the next provider if the first fails before the first token. For most agent loops, non-streaming calls are simpler and the latency gap is acceptable.

Step 3: Forward cache-control and routing hints

Production agents reuse prompts—system instructions, few-shot examples, tool schemas. Provider caching cuts cost and tail latency. The OpenAI-compatible spec passes cache hints via extra_headers. Our **kwargs already forwards them, but be explicit in your agent code:

out = chat.complete(
    messages=[
        {"role": "system", "content": "You are a terse ops bot."},
        {"role": "user", "content": "Status?"},
    ],
    extra_headers={"cache-control": "max-age=300"},
    temperature=0,
)

If a provider ignores the header, the call still succeeds. If you later switch to a gateway that honors client routing directives, the same header works unchanged.

Step 4: Wrap it in a minimal agent loop with tools

A real agent loops: the model emits a tool call, you execute it, you feed the result back. Failover must survive across iterations, not just the first turn. Below is a stripped-down ReAct loop with a dummy weather tool.

def fake_weather(lat: float, lon: float) -> str:
    return "22C, clear"

TOOLS = [{
    "type": "function",
    "function": {
        "name": "fake_weather",
        "parameters": {
            "type": "object",
            "properties": {"lat": {"type": "number"}, "lon": {"type": "number"}},
            "required": ["lat", "lon"],
        },
    },
}]

def run_agent(query: str):
    messages = [{"role": "user", "content": query}]
    for _ in range(5):
        out = chat.complete(messages, tools=TOOLS, tool_choice="auto")
        resp = out["response"]
        msg = resp.choices[0].message
        messages.append(msg)
        if not msg.tool_calls:
            return {"provider": out["provider"], "content": msg.content}
        for call in msg.tool_calls:
            if call.function.name == "fake_weather":
                args = json.loads(call.function.arguments)
                result = fake_weather(**args)
                messages.append({
                    "role": "tool",
                    "tool_call_id": call.id,
                    "content": result,
                })
    return {"provider": "none", "content": "loop exhausted"}

The key point: chat.complete is the only LLM entrypoint. Every iteration gets failover for free, and the provider field tells you which backend actually answered.

Step 5: Force a provider failure to prove fallback

Don’t wait for a real outage. Inject a broken key into the first provider and watch the chain rotate.

PROVIDERS[0]["api_key"] = "sk-broken"
chat = FailoverChat(PROVIDERS)
out = chat.complete([{"role": "user", "content": "Ping"}])
print(out["provider"])  # -> groq

You should see groq printed. Restore the key afterward. In a load test, script this by mocking client.chat.completions.create to raise RateLimitError for the first N calls.

Step 6: When to delegate failover to a gateway

Maintaining the provider list, health checks, and token accounting inside every service is repetitive. If you’d rather not operate that logic, n4n.ai provides one OpenAI-compatible endpoint that addresses 240+ models and performs automatic fallback when a provider is rate-limited or degraded. You point a single client at the gateway and keep the same extra_headers routing hints.

from openai import OpenAI
gateway = OpenAI(
    base_url=os.environ["N4N_BASE_URL"],
    api_key=os.environ["N4N_API_KEY"],
)
# No provider list needed; fallback is server-side.
resp = gateway.chat.completions.create(
    model="auto",  # gateway picks healthy backend
    messages=[{"role": "user", "content": "Ping"}],
    extra_headers={"cache-control": "max-age=300"},
)

Per-token usage metering comes back in the standard usage field, so your existing billing code works unchanged.

Operational checklist

  • Set timeouts on the client (timeout=10) so a hung provider doesn’t block the agent.
  • Log provider from each response; dashboards should show failover rate.
  • Alert when the last provider in the chain is hit—it means primary is unhealthy.
  • Rotate keys via env, never hardcode.
  • For stateful agents, persist messages so a failover mid-loop doesn’t lose context.
  • Add a synthetic canary call in your deploy pipeline that forces a failover to verify the chain.

That’s the whole multi-provider failover tutorial. The pattern is roughly twenty lines of Python, but it’s the difference between a demo and a system that survives a vendor incident at 3 a.m.

Tagsmulti-providerfailoverllm-routingproduction

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All llm routing & fallback for agentic apps posts →