n4nAI

Building a retry-then-fallback chain for LLM API calls

Learn how to build a retry then fallback chain for LLM API calls in Python: classify errors, retry with backoff, fall back across providers, and verify.

n4n Team3 min read571 words

Audio narration

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

When a single LLM provider throws a 429 or a 503, your production traffic shouldn’t wait. A retry then fallback chain llm api pattern lets you absorb transient provider errors by retrying with backoff and then shifting the request to a secondary provider—without blocking the caller. This post walks through building that chain in Python with the OpenAI SDK, so you can ship resilient inference calls today.

Step 1: Classify errors and order your providers

Not every error deserves a retry. Rate limits (HTTP 429) and upstream 5xx responses are transient. Authentication failures (401), invalid requests (400), and context-length violations are permanent for that exact payload. Retry only the transient set; fall back when retries exhaust or a permanent error hits on the primary.

Define your provider list explicitly. Order matters: put the cheapest or lowest-latency provider first, reserve premium models for fallback.

PROVIDERS = [
    {"base_url": "https://api.openai.com/v1", "api_key": "sk-...", "model": "gpt-4o-mini"},
    {"base_url": "https://api.anthropic.com/v1", "api_key": "sk-ant-...", "model": "claude-3-5-sonnet"},
    {"base_url": "https://local-vllm:8000/v1", "api_key": "EMPTY", "model": "mistral-7b"},
]

Each entry is an OpenAI-compatible endpoint. The same client code works across vendors because they expose the /chat/completions shape.

Step 2: Implement retry with exponential backoff

Retry logic must be bounded and jittery. Fixed intervals synchronize failures across concurrent callers; exponential backoff with random jitter spreads them out.

from openai import OpenAI, RateLimitError, APIError
import time, random

def complete_with_retry(client, model, messages, max_retries=3):
    attempt = 0
    while attempt < max_retries:
        try:
            return client.chat.completions.create(model=model, messages=messages)
        except RateLimitError:
            pass  # 429, always retryable
        except APIError as e:
            if not (e.status_code and 500 <= e.status_code < 600):
                raise  # permanent, don't retry
        wait = (2 ** attempt) + random.uniform(0, 1)
        time.sleep(wait)
        attempt += 1
    raise RuntimeError("retry budget exhausted")

The function returns the successful response object or raises after max_retries. It never catches non-transient errors, so a malformed request fails fast instead of burning retries.

Step 3: Build the fallback loop

Wrap the retry function in a loop over providers. Capture the last exception so you can surface a meaningful error if every provider fails.

def retry_then_fallback_chain_llm_api(messages, providers=PROVIDERS):
    last_err = None
    for cfg in providers:
        client = OpenAI(base_url=cfg["base_url"], api_key=cfg["api_key"])
        try:
            resp = complete_with_retry(client, cfg["model"], messages)
            return resp
        except Exception as e:
            last_err = e
            continue
    raise last_err

This is the core of the retry then fallback chain llm api pattern. The caller gets a single response object or a single exception; it doesn’t need to know which provider served the token.

Step 4: Pass cache and routing hints

Production gateways often accept headers to control caching and provider selection. If you front your providers with a unified endpoint, forward cache-control hints so repeated prompts hit provider caches.

def complete_with_retry(client, model, messages, max_retries=3):
    attempt = 0
    while attempt < max_retries:
        try:
            return client.chat.completions.create(
                model=model,
                messages=messages,
                extra_headers={
                    "x-cache-control": "max-age=3600",
                    "x-provider-preference": "openai,anthropic"
                }
            )
        except RateLimitError:
            pass
        except APIError as e:
            if not (e.status_code and 500 <= e.status_code < 600):
                raise
        time.sleep((2 ** attempt) + random.uniform(0, 1))
        attempt += 1
    raise RuntimeError("retry budget exhausted")

Some gateways, including n4n.ai, honor client routing directives and forward provider cache-control hints, so you can pin a model or force a specific provider via headers while still getting fallback across 240+ models behind one OpenAI-compatible endpoint.

Step 5: Meter usage and log attempts

Per-token cost visibility is non-negotiable when you span multiple providers. Extract usage from the response and tag it with the provider that succeeded.

def retry_then_fallback_chain_llm_api(messages, providers=PROVIDERS):
    last_err = None
    for cfg in providers:
        client = OpenAI(base_url=cfg["base_url"], api_key=cfg["api_key"])
        try:
            resp = complete_with_retry(client, cfg["model"], messages)
            usage = resp.usage.model_dump()
            print({"provider": cfg["base_url"], "model": cfg["model"], **usage})
            return resp
        except Exception as e:
            last_err = e
            continue
    raise last_err

If you use a gateway that provides per-token usage metering, the same usage object reflects the underlying provider’s counts without extra plumbing.

Step 6: Verify the chain end to end

A chain is only as good as its test. Mock the client to fail the first provider twice with RateLimitError, then succeed. Assert the second provider is never touched.

import pytest
from openai import RateLimitError
from your_module import complete_with_retry, retry_then_fallback_chain_llm_api

class FakeCompletions:
    def __init__(self, fails):
        self.fails = fails
        self.calls = 0
    def create(self, **kwargs):
        self.calls += 1
        if self.calls <= self.fails:
            raise RateLimitError("rate", response=None, body=None)
        class R:
            usage = type("U", (), {"prompt_tokens":1,"completion_tokens":1,"total_tokens":2})()
        return R()

class FakeClient:
    def __init__(self, fails):
        self.chat = type("Chat", (), {"completions": FakeCompletions(fails)})()

def test_retry_then_fallback(monkeypatch):
    primary = FakeClient(fails=2)
    secondary = FakeClient(fails=99)
    providers = [
        {"base_url": "p1", "api_key": "x", "model": "m1"},
        {"base_url": "p2", "api_key": "x", "model": "m2"},
    ]
    monkeypatch.setattr("your_module.OpenAI", lambda **kw: primary if kw["base_url"]=="p1" else secondary)
    resp = retry_then_fallback_chain_llm_api([{"role":"user","content":"hi"}], providers)
    assert primary.chat.completions.calls == 3
    assert secondary.chat.completions.calls == 0

Run pytest -q and confirm the primary is retried three times (initial plus two retries) and the secondary is never called. Then run an integration test with a deliberately invalid API key on the first provider to force fallback to the second, and watch the usage log print the fallback provider.

Operational notes

Set a hard timeout on the client (timeout=30) so a hung connection doesn’t stall the chain. Use a circuit breaker if a provider shows repeated degradation—skip it for a cooldown period instead of always hitting it first. Keep max_retries low (2–3); fallback is cheaper than long retry storms.

The retry then fallback chain llm api design keeps your service alive when individual providers flap. Implement the steps above, test with mocks, and you have production-grade resilience without a heavyweight framework.

Tagsfallbackretryerror-handlingapi-design

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 multi-provider fallback code patterns posts →