Keep Claude Running Through Overload

Build a Claude client that retries, reroutes, and falls back during overloads.

Introduction

30 Second Summary

Online services sometimes get too busy to answer every request. Without a recovery plan, one temporary capacity problem can stop an otherwise useful script.

In this project, you will build a small Python command line script that sends prompts to Claude through OpenRouter. Its recovery path moves from timed retries to provider rerouting before a fallback model answers.

What You'll Build

Your finished script keeps sending a prompt through recovery routes until the terminal shows a successful response with the route that answered.

By the end of this project, you'll have:

  • Matched a successful Claude response to its recorded cost on OpenRouter's activity page.
  • Watched exponential backoff turn a simulated 529 overloaded error into an attempt log with growing wait times.
  • Run a ten-call failover test to see which provider or fallback model answered each request.
  • Secret Mission: An optional challenge to push your resilience skills further.

Are there any prerequisites?

This project assumes you have Python available on your computer plus access to a terminal.

Step 1 walks you through creating an OpenRouter account, generating a key, and adding a small prepaid balance.

Before We Start

A resilient client still needs a reliable local starting point. Missing runtimes or packages can look like provider failures later.

In this step, you will prepare a Python 3 environment with Requests installed. You will also confirm that OpenRouter's Activity dashboard is accessible.

In this step, get ready to:
  • Verify that Python 3 is available.
  • Prepare an empty project folder with Requests installed.
  • Confirm that you can access the OpenRouter Activity dashboard.
Verify your Python installation

Python runs every script in this project. A version check confirms that your terminal can find a Python 3 interpreter.

macOS

  • Press Cmd+Space to open Spotlight.
  • Type Terminal into Spotlight.
  • Press Enter to open Terminal.
  • Check for Python 3 by running this command:
python3 --version

What does this check do?

  • The python3 command selects the Python 3 interpreter.
  • The --version flag prints its installed version.

Windows

  • Press the Windows key to open Windows Search.
  • Type PowerShell into Windows Search.
  • Press Enter to open PowerShell.
  • Check for Python 3 by running this command:
py --version

What does this check do?

  • The py command launches Python on Windows.
  • The --version flag prints its installed version.

✔️ I see a Python 3 version

Good. Python 3 is ready to run your project scripts.

ⓧ Command not found

Python needs to be installed before you can prepare the project environment. Choose your operating system below.

macOS

  • Open the official Python downloads page for macOS in your browser.
  • Select Download macOS installer for the latest stable release.
  • Open the downloaded installer package.
  • Click Continue in the installer.
  • Click Agree after reviewing the licence.
  • Click Install to start the installation.
  • Approve the administrator prompt to complete the installation.
Finish the certificate setup

The Python installer includes a certificate setup utility. This prepares Python to make secure web requests.

  • Double-click Install Certificates.command in the Python folder shown after installation.
  • Return to Terminal.
  • Confirm the installation by running the version check again:
python3 --version
What confirms success?

The command prints a Python 3 version. Your terminal can now find the installed interpreter.

Windows

  • Open the official Python downloads page for Windows in your browser.
  • Select Download Installer (MSIX) under the latest Python install manager release.
  • Open the downloaded installer.
  • Click Install to add the Python install manager.
  • Approve the Windows installation prompt.
The first launch can take longer

The Python install manager can install the latest stable runtime during its first use. Allow the download to finish before checking the version.

  • Return to PowerShell.
  • Confirm the installation by running the version check again:
py --version
What confirms success?

The command prints a Python 3 version. PowerShell can now find the installed interpreter.

Prepare the project environment

A dedicated folder keeps the resilience client separate from your other files. Requests gives Python the HTTP features needed to contact OpenRouter.

macOS

  • Create the empty claude-resilience-client folder on your Desktop by running these commands:
cd ~/Desktop
mkdir claude-resilience-client
cd claude-resilience-client
pwd

What do these commands do?

  • The first cd command moves your terminal to the Desktop.
  • The mkdir command creates the project folder.
  • The second cd command enters the new folder.
  • The pwd command prints your current location.

The printed path should end with /Desktop/claude-resilience-client.

  • Install Requests for your Python interpreter by running this command:
python3 -m pip install requests

What does this installation do?

The pip package installer downloads Requests. Python can then import the library in your later scripts.

  • Confirm that Requests is available by running this command:
python3 -m pip show requests

What does this package check show?

The show command reads the installed package metadata. It includes the package name plus its installed version.

You should see Name: requests. A Version field should also appear.

Windows

  • Create the empty claude-resilience-client folder on your Desktop by running these commands:
Set-Location ~/Desktop
New-Item -Name claude-resilience-client -ItemType Directory
Set-Location claude-resilience-client
Get-Location

What do these commands do?

  • The first Set-Location command moves PowerShell to the Desktop.
  • The New-Item command creates the project folder.
  • The second Set-Location command enters the new folder.
  • The Get-Location command prints your current location.

The printed path should end with Desktop\claude-resilience-client.

  • Install Requests for your Python interpreter by running this command:
py -m pip install requests

What does this installation do?

The pip package installer downloads Requests. Python can then import the library in your later scripts.

  • Confirm that Requests is available by running this command:
py -m pip show requests

What does this package check show?

The show command reads the installed package metadata. It includes the package name plus its installed version.

You should see Name: requests. A Version field should also appear.

Requests missing after installation?

Use the same Python launcher that worked during your version check. This keeps the installation tied to the interpreter you plan to run.

Confirm that your internet connection allows package downloads. A blocked download leaves Requests unavailable.

Help me troubleshoot my Requests installation

Confirm OpenRouter Activity access

The Activity dashboard records spend. It also records request volume. Later steps use it to show which provider handled each call.

Before you open Activity, predict the usage information you expect a fresh account to display.

You should see a Spend metric. You should also see a Tokens metric.

You should see a Requests metric. These sections prove that Activity is ready to record your later calls.

You are set. Your terminal can run the client while OpenRouter provides a separate record of each request.

Activity page not loading?

Confirm that you completed OpenRouter's account access flow. A private browser window can help if an old session keeps redirecting you.

Help me access OpenRouter Activity

✔️ Awesome, I've got everything

Your empty project environment is ready. Keep the terminal open inside claude-resilience-client.

ⓧ I'd like to double check the full setup

  • Confirm that Python 3 returns a version number.
  • Confirm that your terminal path ends with claude-resilience-client.
  • Confirm that the Requests package details include Name: requests.
  • Confirm that OpenRouter Activity displays its usage metrics.

Your local environment is ready. Next, you will create your OpenRouter key. Then you will send your first Claude request.

Send Your First Claude Request

A recovery client needs a known-good baseline. One successful API request proves that your route works before you simulate failures.

In this step, you'll sign in to OpenRouter. You'll create a capped API key with a small credit balance.

You'll send one short prompt to Claude. The Activity page then shows the request cost.

In this step, get ready to:
  • Fund your OpenRouter account with a small credit balance.
  • Create a capped API key for your terminal requests.
  • Send a Claude request and confirm its recorded cost.
Fund your OpenRouter account and create a key

OpenRouter deducts each model request from your credit balance. A small deposit is enough for this project's short prompts.

The next action charges your payment method. The checkout page shows the full total before you confirm it.

  • Switch back to the OpenRouter website from earlier.
  • Visit the OpenRouter Credits page.
  • Complete the authentication flow shown by OpenRouter.
  • Select the page option for purchasing credits.
  • Choose a small purchase amount: your small credit amount in USD.
  • Complete the checkout prompts.
  • Confirm that your new credit balance appears on the Credits page.

Your balance is now ready to cover the first Claude call.

An API key lets the terminal authenticate with your OpenRouter account. Its credit limit keeps this project's spending within the amount you choose.

The generated key appears once, so pause before closing its dialog.

  • Visit the OpenRouter API Keys page.
  • Select Create API Key.
  • Enter Claude resilience project as the key name.
  • Set the Credit limit to your small credit amount in USD.
  • Complete the key creation using the page's confirmation control.
  • Copy the generated key before closing the dialog.

Why cap the key?

A credit limit caps how much this key can spend if it leaks. It also protects the rest of your OpenRouter balance.

Store the key in your terminal

The OPENROUTER_API_KEY environment variable holds the credential for your current terminal session. Your request can read the key without placing it directly in the request code.

macOS

  • Replace sk-or-v1-YOUR-API-KEY in the command below with the key you copied.
  • Store the edited value in OPENROUTER_API_KEY by running the command:
export OPENROUTER_API_KEY="sk-or-v1-YOUR-API-KEY"

What does this command do?

The command stores your key in OPENROUTER_API_KEY for this terminal session. Commands launched from the same terminal can now authenticate without displaying the key.

Windows

  • Replace sk-or-v1-YOUR-API-KEY in the command below with the key you copied.
  • Store the edited value in OPENROUTER_API_KEY by running the command:
$env:OPENROUTER_API_KEY="sk-or-v1-YOUR-API-KEY"

What does this command do?

The command stores your key in OPENROUTER_API_KEY for this PowerShell session. Commands launched from the same terminal can now authenticate without displaying the key.

Send a Claude request and verify its cost

This smoke test sends a short prompt through OpenRouter. The response prints enough information to prove that your key and credit balance work.

Before you run the request, do you expect Claude to return a normal answer or an error?

macOS

  • Send the first Claude request from the terminal session that contains your key by running:
python3 - <<'PY'
import os

import requests

# Send a short prompt without putting the API key in this script.
response = requests.post(
    "https://openrouter.ai/api/v1/chat/completions",
    json={
        "model": "anthropic/claude-sonnet-5.5",
        "messages": [{"role": "user", "content": "Reply with the word ok."}],
        "max_tokens": 16,
    },
    timeout=120,
    headers={"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}"},
)
response.raise_for_status()

# Print the status, model, and reply to make the successful route visible.
data = response.json()
print(response.status_code, data["model"], data["choices"][0]["message"]["content"])
PY

What does this request do?

  • The request sends one user message to OpenRouter's chat completions endpoint.
  • The model value selects anthropic/claude-sonnet-5.5.
  • The authorization header reads the key from OPENROUTER_API_KEY.
  • The final line prints the status code.
  • The final line also prints the model name.
  • The final line finishes with Claude's reply.

Windows

  • Send the first Claude request from the PowerShell session that contains your key by running:
@'
import os

import requests

# Send a short prompt without putting the API key in this script.
response = requests.post(
    "https://openrouter.ai/api/v1/chat/completions",
    json={
        "model": "anthropic/claude-sonnet-5.5",
        "messages": [{"role": "user", "content": "Reply with the word ok."}],
        "max_tokens": 16,
    },
    timeout=120,
    headers={"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}"},
)
response.raise_for_status()

# Print the status, model, and reply to make the successful route visible.
data = response.json()
print(response.status_code, data["model"], data["choices"][0]["message"]["content"])
'@ | python

What does this request do?

  • The request sends one user message to OpenRouter's chat completions endpoint.
  • The model value selects anthropic/claude-sonnet-5.5.
  • The authorization header reads the key from OPENROUTER_API_KEY.
  • The final line prints the status code.
  • The final line also prints the model name.
  • The final line finishes with Claude's reply.

You should see 200 first. The next value names the Claude model that answered.

The final value is a short reply containing ok.

Request did not succeed?

If the terminal reports an authentication problem, set OPENROUTER_API_KEY again in the same terminal session. Make sure the copied key has no spaces around it.

If Python cannot import requests, your terminal may be using a different Python installation from the one prepared earlier.

Help me diagnose my first OpenRouter request.

Before you refresh the Activity page, do you expect this request to have a recorded cost?

  • Switch back to the OpenRouter Activity page from earlier.
  • Refresh the Activity page.
  • Locate the newest request entry.
  • Confirm that the entry shows the Claude model.
  • Confirm that the entry shows the request cost.

Your newest Activity entry should show the Claude request with its cost. This confirms that OpenRouter processed the call against your credit balance.

Your first Claude call now has a clean response with a recorded cost. Next, you'll simulate an overloaded model so you can see the failure your client needs to survive.

Simulate an Overloaded Claude Endpoint

Your first OpenRouter request proved that Claude can answer when capacity is available. A real overload arrives unpredictably, which makes recovery code difficult to test.

This step creates a local stand-in that always returns an HTTP 529 response. The request stays on your computer, so it does not use your API key or spend your credit balance.

In this step, get ready to:
  • Build a local endpoint that returns HTTP 529.
  • Create a client that sends a Claude-shaped request to the endpoint.
  • Capture the raw overloaded error in your terminal.
Build the fake overloaded endpoint

The fake endpoint gives you a repeatable failure signal. It uses Python's local HTTP server to return the same error body every time it receives a request.

  • Create a file named fake_overload.py inside the project folder from earlier.
  • Paste the following code into fake_overload.py:
import json
from http.server import BaseHTTPRequestHandler, HTTPServer

# Match the error body Claude returns during a capacity overload.
ERROR_BODY = {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}

class OverloadedHandler(BaseHTTPRequestHandler):
    # Return the same simulated overload for every POST request.
    def do_POST(self):
        body = json.dumps(ERROR_BODY).encode("utf-8")
        self.send_response(529)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

if __name__ == "__main__":
    # Keep the local test endpoint available for the client.
    server = HTTPServer(("127.0.0.1", 8529), OverloadedHandler)
    print("Fake endpoint running at http://127.0.0.1:8529/v1/messages", flush=True)
    server.serve_forever()

What Does This Code Do?

  • The ERROR_BODY dictionary holds the raw JSON error that the endpoint returns.
  • The do_POST() method handles each incoming request with status code 529.
  • The Content-Length header tells the client how many response bytes to read.
  • The local server listens on port 8529 until you stop it.
  • Save fake_overload.py.
  • Start the endpoint in the background by running this command:
python3 fake_overload.py &

What Should You See?

The ampersand keeps the endpoint running while returning control of the terminal to you. The startup message confirms the local URL that accepts test requests.

Good. Your local endpoint is now waiting for requests at http://127.0.0.1:8529/v1/messages.

Endpoint Did Not Start?

Confirm that fake_overload.py is saved in the project folder shown by your terminal. A different current folder prevents Python from finding the file.

If the terminal reports that the port is already in use, another copy of the endpoint may still be running. Help me find the process using port 8529.

✔️ Awesome, I've got everything!

Your fake endpoint is saved. Keep it running for the client test.

ⓧ I'd like to double check the full code

Compare your complete fake_overload.py file with this version.

import json
from http.server import BaseHTTPRequestHandler, HTTPServer

# Match the error body Claude returns during a capacity overload.
ERROR_BODY = {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}

class OverloadedHandler(BaseHTTPRequestHandler):
    # Return the same simulated overload for every POST request.
    def do_POST(self):
        body = json.dumps(ERROR_BODY).encode("utf-8")
        self.send_response(529)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

if __name__ == "__main__":
    # Keep the local test endpoint available for the client.
    server = HTTPServer(("127.0.0.1", 8529), OverloadedHandler)
    print("Fake endpoint running at http://127.0.0.1:8529/v1/messages", flush=True)
    server.serve_forever()
Point the client at the endpoint

The client needs a configurable URL so the same request logic can target the local test endpoint now. The Requests library sends the prompt and exposes both the status code and response body.

  • Create a file named claude_fallback.py inside the same project folder.
  • Paste the following code into claude_fallback.py:
import requests

# Keep the endpoint configurable for later routing changes.
URL = "http://127.0.0.1:8529/v1/messages"

# Send the same request shape on every resilience test.
BODY = {
    "model": "claude-sonnet-5-5",
    "max_tokens": 64,
    "messages": [{"role": "user", "content": "hi"}],
}

# Print the complete response so failures stay visible.
response = requests.post(URL, json=BODY, timeout=60)
print(f"HTTP {response.status_code}")
print(response.text)

What Does This Code Do?

  • The URL value points the client at the local endpoint.
  • The BODY dictionary carries a model name and a short prompt.
  • The requests.post() call sends the body as JSON.
  • The final two lines preserve the status code and raw response text in your terminal.
  • Save claude_fallback.py.

Before you run the client, do you think the local endpoint will return a Claude response or an overload error?

  • Send the test request by running this command:
python3 claude_fallback.py

What Did This Test Prove?

The client reached the local endpoint and received HTTP 529. The response body preserved the overloaded_error type that future retry logic can detect.

You should see HTTP 529 followed by {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}. This failure is intentional.

Client Cannot Reach the Endpoint?

Check that the terminal still shows the fake endpoint process as running. Restart fake_overload.py if that process stopped.

Confirm that the URL in claude_fallback.py uses port 8529. Help me debug the connection between these two Python files.

✔️ Awesome, I've got everything!

Your client now exposes the simulated overload in the terminal. Double check that claude_fallback.py is saved.

ⓧ I'd like to double check the full code

Compare your complete claude_fallback.py file with this version.

import requests

# Keep the endpoint configurable for later routing changes.
URL = "http://127.0.0.1:8529/v1/messages"

# Send the same request shape on every resilience test.
BODY = {
    "model": "claude-sonnet-5-5",
    "max_tokens": 64,
    "messages": [{"role": "user", "content": "hi"}],
}

# Print the complete response so failures stay visible.
response = requests.post(URL, json=BODY, timeout=60)
print(f"HTTP {response.status_code}")
print(response.text)

You have captured a repeatable Claude overload without waiting for a real capacity spike. Next, you will add controlled retries with growing wait times.

Retry with Backoff and Jitter

Your local endpoint has already exposed the raw 529 response that stops the client. Short capacity spikes can clear within seconds, so the client needs a controlled way to try again.

In this step, you will add exponential backoff to increase the delay after each failure. You will also add jitter to keep many clients from retrying at the same instant.

In this step, get ready to:
  • Add a bounded retry loop for HTTP 529 responses.
  • Calculate growing wait times with random jitter.
  • Verify each attempt in the terminal logs.
Add bounded retries to the client

A retry loop gives a temporary overload time to clear. A maximum attempt count prevents the script from retrying forever.

  • Replace the contents of claude_fallback.py with the code below.
import random
import time
import requests

# The local stand-in. In production, use https://api.anthropic.com/v1/messages
URL = "http://127.0.0.1:8529/v1/messages"

def call_with_backoff(body, attempts=5):
    for attempt in range(attempts):
        r = requests.post(URL, json=body, timeout=60)
        if r.status_code != 529:
            return r
        if attempt == attempts - 1:
            r.raise_for_status()  # out of attempts: fail now instead of sleeping again
        wait = min(2 ** attempt, 30) + random.uniform(0, 1)
        print(f"attempt {attempt + 1}: 529 {r.json()['error']['type']}, retrying in {wait:.1f}s")
        time.sleep(wait)

r = call_with_backoff({
    "model": "claude-sonnet-5-5",
    "max_tokens": 64,
    "messages": [{"role": "user", "content": "hi"}],
})
print(r.status_code, r.json()["content"][0]["text"])

What Does This Code Do?

The retry loop handles short capacity spikes while keeping the number of requests under control.

  • The URL value keeps the client pointed at the local test endpoint.
  • The call_with_backoff() function makes up to five attempts.
  • A response with a status other than 529 returns immediately.
  • The 2 ** attempt calculation doubles the base delay after each failed attempt.
  • The random.uniform(0, 1) value adds up to one second of jitter.
  • The final failed attempt raises the response error without adding another wait.
  • Save claude_fallback.py.

Before you run the client, do you expect the retry delay to stay flat or grow after each 529 response?

  • Test the retry loop against the fake endpoint by running this command:
python3 claude_fallback.py

What Should I See?

You will see an attempt number followed by 529 overloaded_error for each failed request. The displayed delays grow from roughly one second to two seconds to four seconds.

No Retry Logs?

Confirm the local fake endpoint is still running. The client needs that endpoint to produce the controlled 529 response.

Check that URL still matches the local endpoint from the previous step. Help me troubleshoot why claude_fallback.py does not print its retry logs.

Confirm the jitter changes each run

Exponential backoff determines the growing base delay. Jitter changes the decimal portion so separate clients are less likely to send their next requests together.

Before you run the client again, do you expect the decimal wait times to match your first run exactly?

  • Run the same client again to generate a second set of wait times:
python3 claude_fallback.py

How Does the Second Run Compare?

You will see the same growing pattern with different decimal values. This variation proves that jitter is shifting each retry within its delay window.

That is the recovery loop working. Your client now waits longer after repeated overloads without making every retry land at the same moment.

✔️ Awesome, I've got everything!

Great. Double check that you saved claude_fallback.py after confirming the growing wait times.

ⓧ I'd like to double check the full code

Compare your complete claude_fallback.py file with this version.

import random
import time
import requests

# The local stand-in. In production, use https://api.anthropic.com/v1/messages
URL = "http://127.0.0.1:8529/v1/messages"

def call_with_backoff(body, attempts=5):
    for attempt in range(attempts):
        r = requests.post(URL, json=body, timeout=60)
        if r.status_code != 529:
            return r
        if attempt == attempts - 1:
            r.raise_for_status()  # out of attempts: fail now instead of sleeping again
        wait = min(2 ** attempt, 30) + random.uniform(0, 1)
        print(f"attempt {attempt + 1}: 529 {r.json()['error']['type']}, retrying in {wait:.1f}s")
        time.sleep(wait)

r = call_with_backoff({
    "model": "claude-sonnet-5-5",
    "max_tokens": 64,
    "messages": [{"role": "user", "content": "hi"}],
})
print(r.status_code, r.json()["content"][0]["text"])

Your client can now absorb a short overload without hammering the endpoint. Next up, you will use OpenRouter to reroute the same model when retries are not enough.

Enable Provider Fallback

Your retry loop now survives short 529 capacity spikes. A longer outage can still leave one provider unavailable after every wait.

This step moves the request to OpenRouter's provider routing. Your selected Claude model stays fixed while another provider can answer.

In this step, get ready to:
  • Point the client at OpenRouter with provider fallback enabled.
  • Log which provider serves each successful request.
  • Confirm the provider route in OpenRouter Activity.
Configure OpenRouter routing

A provider object tells OpenRouter how to route one model across its available providers. The allow_fallbacks setting lets another provider handle the request when an earlier route fails.

Why Provider Fallback?

Retries handle brief interruptions by waiting before another attempt. Provider fallback adds a second recovery path by moving the same model request to different infrastructure.

This keeps the model consistent while reducing your dependence on one provider's capacity.

  • Return to claude_fallback.py in your code editor.
  • Select everything from the first import through the end of call_with_backoff().
  • Replace the selected code with this updated request and retry logic:
import os
import random
import time

import requests

# Send every attempt through OpenRouter.
URL = "https://openrouter.ai/api/v1/chat/completions"
MODEL = "anthropic/claude-sonnet-5.5"

def call_with_backoff(body, attempts=5):
    # The API key authorizes each routed request.
    headers = {"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}"}
    for attempt in range(attempts):
        print(f"attempt {attempt + 1}: sending {MODEL}")
        r = requests.post(URL, json=body, timeout=120, headers=headers)
        if r.status_code != 529:
            r.raise_for_status()
            return r
        error_type = r.json()["error"]["type"]
        if attempt == attempts - 1:
            r.raise_for_status()
        wait = min(2 ** attempt, 30) + random.uniform(0, 1)
        print(f"attempt {attempt + 1}: 529 {error_type}, retrying in {wait:.1f}s")
        time.sleep(wait)

What Does This Code Do?

  • The URL sends each attempt to OpenRouter's chat completions endpoint.
  • The Authorization header reads the key from OPENROUTER_API_KEY.
  • The loop preserves your exponential backoff for 529 responses. It returns after a different status passes its status check.
  • Save claude_fallback.py.
  • Confirm the updated function sends requests to the OpenRouter URL with the API key header.

Function Structure Look Wrong?

Check that time.sleep(wait) remains indented inside the retry loop. Make sure the request line includes headers=headers.

Python uses indentation to determine where the retry loop ends.

help me check the indentation and request headers in my updated call_with_backoff function

Add provider preferences and route logging

The provider preferences keep Anthropic first in the route order. OpenRouter can continue to the remaining providers when allow_fallbacks is set to True.

  • Select everything after the call_with_backoff() function in claude_fallback.py.
  • Replace the selected code with this routed request and response logging:
# Keep the model fixed while OpenRouter can change its provider.
body = {
    "model": MODEL,
    "provider": {
        "order": ["anthropic", "amazon-bedrock", "google-vertex"],
        "allow_fallbacks": True,
    },
    "messages": [{"role": "user", "content": "Reply with the word ok."}],
    "max_tokens": 16,
}

response = call_with_backoff(body)
data = response.json()
print(f"provider: {data['provider']}")
print(f"response: {data['choices'][0]['message']['content']}")

How Does the Route Work?

  • The order list gives OpenRouter the first providers to try for the selected model.
  • The allow_fallbacks value permits another available provider after those preferred routes fail.
  • The response's provider field identifies the provider that completed the request.
  • Save claude_fallback.py.
  • Confirm the request body contains the provider object.
  • Confirm the final lines print the provider name and response text.

Provider Block Misaligned?

Keep the provider object inside body. Align it with model and messages.

Check that every opening brace has a matching closing brace.

help me check the provider object in my OpenRouter request body

Run and confirm the provider route

This uses a tiny slice of your prepaid balance because the output is capped at 16 tokens.

Before you run the client, do you expect the model name or the serving provider to change between routes?

  • Run the routed request from your open project terminal with this command:
python3 claude_fallback.py

You should see an attempt line followed by a provider name. The final response should contain ok.

That is the routing layer working. Your client now records which provider successfully answered the Claude request.

Request Did Not Succeed?

If the error mentions authentication, confirm OPENROUTER_API_KEY is still available in this terminal. Avoid printing the key itself.

If the error points to the request body, compare the model name and provider slugs with the full code below.

help me diagnose my OpenRouter provider fallback request without sharing my API key

Before you check Activity, which provider do you expect to match the terminal log?

  • Return to the OpenRouter Activity page from earlier.
  • Refresh the Activity page.
  • Select the newest successful request entry.
  • Compare its serving provider with the provider printed in your terminal.

You should see the same provider in both places. This confirms OpenRouter served the request through one of the available routes.

✔️ Awesome, I've got everything!

Great. Double check that you saved claude_fallback.py.

ⓧ I'd like to double check the full code

Compare your complete claude_fallback.py file with this version.

import os
import random
import time

import requests

# Send every attempt through OpenRouter.
URL = "https://openrouter.ai/api/v1/chat/completions"
MODEL = "anthropic/claude-sonnet-5.5"

def call_with_backoff(body, attempts=5):
    # The API key authorizes each routed request.
    headers = {"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}"}
    for attempt in range(attempts):
        print(f"attempt {attempt + 1}: sending {MODEL}")
        r = requests.post(URL, json=body, timeout=120, headers=headers)
        if r.status_code != 529:
            r.raise_for_status()
            return r
        error_type = r.json()["error"]["type"]
        if attempt == attempts - 1:
            r.raise_for_status()
        wait = min(2 ** attempt, 30) + random.uniform(0, 1)
        print(f"attempt {attempt + 1}: 529 {error_type}, retrying in {wait:.1f}s")
        time.sleep(wait)

# Keep the model fixed while OpenRouter can change its provider.
body = {
    "model": MODEL,
    "provider": {
        "order": ["anthropic", "amazon-bedrock", "google-vertex"],
        "allow_fallbacks": True,
    },
    "messages": [{"role": "user", "content": "Reply with the word ok."}],
    "max_tokens": 16,
}

response = call_with_backoff(body)
data = response.json()
print(f"provider: {data['provider']}")
print(f"response: {data['choices'][0]['message']['content']}")

Your client can now keep one Claude model moving across providers. Next, you will add a second model for the case when every provider for the first model is unavailable.

Add a Fallback Model

Provider fallback keeps one Claude model reachable when a host has trouble. A whole-model outage leaves every provider route with the same dead end.

Your retry layer already handles short overloads. Your OpenRouter routing already tries other providers. This step adds a model fallback that moves the request to a second Claude model.

In this step, get ready to:
  • Add an ordered fallback model to the request.
  • Log the model that answers each request.
  • Force the primary route to fail so the fallback answers.
Add the fallback model

The models array stores model IDs in priority order. OpenRouter tries the next entry when the first model returns an eligible error.

  • Return to claude_fallback.py from the previous step.
  • Locate this primary model entry in the request body:
"model": "anthropic/claude-sonnet-5.5",

What does this line do?

The singular model field gives OpenRouter one model choice. Provider fallback can still move that model between eligible hosts.

  • Replace the singular model entry with this ordered model list:
"models": ["anthropic/claude-sonnet-5.5", "anthropic/claude-haiku-5.5"],

How does the model order work?

  • OpenRouter tries anthropic/claude-sonnet-5.5 first.
  • OpenRouter tries anthropic/claude-haiku-5.5 after an eligible primary-model error.
  • The response's model field identifies the model that ultimately answered.

Your existing logs already show the provider route. Adding the response model makes movement between models visible too.

  • Find the successful response log that prints the status plus the provider:
print(r.status_code, data["provider"])

What is missing from this log?

This line identifies the provider that served the request. It does not identify which model OpenRouter selected.

  • Replace the provider-only log with this model-aware log:
print(r.status_code, data["model"], data["provider"])

What does the updated log show?

The status confirms whether the request succeeded. The next value names the answering model.

The final value names the provider route. One line now exposes both recovery layers.

  • Save claude_fallback.py.

This request uses your credit

The next run sends a live request through OpenRouter. Your key limit keeps this short test bounded.

Before you run the script, which model do you expect to answer while the primary model is available?

  • Send a request with the new model chain by running this command:
python3 claude_fallback.py

What should you see?

You should see a successful status followed by a Claude Sonnet model name. The same line also names the provider route.

Your prompt response appears after the route log. The primary model answered because it was available.

Missing the model name?

Check that the successful response log reads from data["model"]. Confirm that this log runs after data receives the response body.

If the script reports an authentication problem, confirm the earlier terminal still has access to the stored environment variable. Do not print its value.

Help me fix the answering-model log in claude_fallback.py.

Force the primary route to fail

A healthy primary model does not exercise the second entry. This controlled test temporarily places a restricted model first so OpenRouter has to continue through the list.

  • Locate the production model list you just added:
"models": ["anthropic/claude-sonnet-5.5", "anthropic/claude-haiku-5.5"],

What does this production chain protect?

Claude Sonnet remains the preferred model. Claude Haiku becomes the recovery path when Sonnet cannot answer.

  • Replace the first model with the restricted test model so the list looks like this:
"models": ["anthropic/claude-fable-5", "anthropic/claude-haiku-5.5"],

Why does this trigger the fallback?

The restricted first entry cannot serve the request. OpenRouter continues to the next eligible model in the ordered list.

The existing provider configuration still applies. OpenRouter selects an eligible provider for the fallback model.

  • Save claude_fallback.py.

Before you run this test, do you expect the request to stop at the failed primary route or return an answer from the second model?

  • Trigger the forced fallback by running this command:
python3 claude_fallback.py

What should you see?

The request should still succeed. The route log should name a Claude Haiku model as the answer source.

You should also see the provider that served the Haiku request. Your prompt receives a response despite the failed first route.

That is the recovery path proven. Your client can now move beyond an unavailable primary model without dropping the request.

Fallback request did not succeed?

Confirm that the request uses the plural models field. A singular field sends only one model choice.

Check that anthropic/claude-haiku-5.5 is the second entry. The fallback list is processed in order.

Help me troubleshoot my forced OpenRouter model fallback.

Restore the production model chain

The restricted model served its purpose as a failure test. Your reusable client should prefer Claude Sonnet during normal operation.

  • Replace the restricted test list with this production list:
"models": ["anthropic/claude-sonnet-5.5", "anthropic/claude-haiku-5.5"],

What does the final chain do?

The first entry keeps Claude Sonnet as the preferred model. The second entry gives OpenRouter a separate Claude model when Sonnet fails.

  • Save claude_fallback.py.

Before the final run, which model do you expect OpenRouter to choose when both entries are available?

  • Verify the restored production chain by running this command:
python3 claude_fallback.py

What should you see?

You should see a successful response with an answering model plus a provider route. Claude Sonnet normally answers when it is available.

A Claude Haiku model name also represents success if a real fallback occurs. The logged model tells you which path OpenRouter used.

  • Return to the OpenRouter Activity page from earlier.
  • Open the newest successful request.

You should see the model that answered the request. You should also see the provider that served it.

✔️ Awesome, I've got everything!

Your saved script now prefers Claude Sonnet. It falls back to Claude Haiku when the primary model cannot answer.

ⓧ I'd like to double check the full code

Keep the retry logic from the earlier steps unchanged. In the request body inside claude_fallback.py the final model entry should match this line.

"models": ["anthropic/claude-sonnet-5.5", "anthropic/claude-haiku-5.5"],

This ordered list preserves Claude Sonnet as the primary model. Claude Haiku remains the second-model fallback.

The successful response log should also match this line.

print(r.status_code, data["model"], data["provider"])

This log prints the request status. It also prints the answering model plus the provider route.

Activity does not show the request?

Refresh the Activity page after the terminal request finishes. Confirm that you are viewing the same OpenRouter account that owns the key.

Help me match my OpenRouter Activity entry to my latest script run.

Your client now recovers across providers or models. Next up, you will run repeated requests to measure which route answers each call.

Run and Summarize Ten Calls

Your OpenRouter client can already recover when its first route fails. A single success still leaves one question: does that recovery path remain dependable across repeated calls?

This final step turns one request into a ten-call batch. A summary table makes every answering route easy to compare.

In this step, get ready to:
  • Run the existing resilience path ten times automatically.
  • Collect the answering route for every completed call.
  • Print a summary table for the full batch.
Add the ten-call batch

A batch turns your reusable client into a repeatable resilience check. Each iteration must use the same retry path that already handles overloaded responses.

The summary table needs one stored record per call. Each record carries the call number. It also carries the answering model plus the provider route.

  • Extend the existing main entry point in claude_fallback.py with a ten-call loop backed by a results collection that prints a route summary after the final call.
  • Save claude_fallback.py.

What should the batch preserve?

  • Every loop iteration uses your existing retry path.
  • Every loop iteration keeps provider fallback enabled.
  • Every loop iteration keeps the second model available.
  • Every completed call contributes one row to the final table.
Run the batch and inspect the routes

This check sends ten real requests against your prepaid credit. The fixed batch size keeps the test bounded.

Before you run the script, do you expect every call to use the same route?

  • Run the completed client from the terminal in your project folder by entering this command:
python3 claude_fallback.py

What does this run prove?

  • The attempt logs prove that every call passed through the resilience path.
  • The final table compresses the ten outcomes into one view.
  • Each row identifies the model that answered its call.
  • Each row identifies the provider route that served its call.

Missing calls or summary rows?

  • Check that the loop runs exactly ten times if fewer than ten calls appear.
  • Make sure each successful call adds a route record before the next iteration begins.
  • Place the summary output after the loop if the table prints repeatedly.
  • Help me troubleshoot my ten-call loop and route summary.
  • Return to the OpenRouter Activity page from earlier.
  • Confirm that the successful requests from this run are listed.
  • Compare the serving providers with the provider routes in your summary table.

You now have ten terminal rows backed by matching OpenRouter activity records. Your reusable client shows which route handled every call.

✔️ Awesome, I've got everything!

Your ten-call summary matches the successful requests on the OpenRouter Activity page.

ⓧ I'd like to double check the full code

Your complete claude_fallback.py has the following behavior:

  • It reads the OpenRouter key from OPENROUTER_API_KEY.
  • It detects overloaded responses with HTTP 529.
  • It retries failed attempts with exponential backoff plus random jitter.
  • It enables provider fallback for the primary Claude model.
  • It provides a second model when the primary model cannot answer.
  • It logs every attempt plus each waiting period.
  • It logs the answering model plus the provider route.
  • It runs the request path exactly ten times.
  • It prints one summary row for every completed call.

Secret mission

Run a Failover Fire Drill

Prove that your recovery client behaves correctly during two controlled incidents. You will capture retry evidence from the local HTTP 529 endpoint before confirming that a forced primary-route failure reaches the second model.

Clean Up Your Resources

Clean Up Your Resources

Your local files do not create ongoing charges. Decide whether to keep, pause, or remove the project resources because the funded OpenRouter API key can still spend its remaining credit.

Cost warning

Requests sent with the active OpenRouter API key can consume its prepaid credit. A spending cap or key deletion prevents leftover credit from being spent through this credential.

  • Choose the Pause or Delete option below when you finish testing.

Resources you used:

  • The claude_fallback.py client in your current project folder.
  • The local fake endpoint process that returns HTTP 529 with an overloaded_error response.
  • The funded OpenRouter API key.
  • The OPENROUTER_API_KEY environment variable stored locally.

Keep everything running

Keep this setup if you want to continue testing fallback routes or reuse the client for another prompt.

  • Leave claude_fallback.py in your current project folder.
  • Keep the local fake endpoint running while you actively test overloaded responses.
  • Keep the OpenRouter key active with the smallest spending cap you need.
  • Retain OPENROUTER_API_KEY in its current local storage location.

Pause - I'll come back to this later

Pause the live parts of the project while keeping your reusable client ready for another session.

  • Switch back to the terminal session running the fake endpoint.
  • Press Ctrl+C to stop the endpoint process.
  • Return to the OpenRouter key page from earlier.
  • Use the key's management controls to cap future spending.
  • Leave claude_fallback.py in your current project folder for your next session.

Your client remains available without leaving the local test endpoint running.

Delete - I don't want to use this again

Removing the key ends access through that credential. The steps below also delete the local client for a full reset.

  • Switch back to the terminal session running the fake endpoint.
  • Press Ctrl+C to stop the endpoint process.
  • Return to the OpenRouter key page from earlier.
  • Locate the funded key you created for this project.
  • Use that key's management controls to delete it.
  • Confirm the project key no longer appears in your OpenRouter key list.

The spending risk is now contained. The deleted key can no longer authorize project requests.

  • Remove the OPENROUTER_API_KEY value from the place where you stored it locally.
  • Use your computer's file browser to search for claude_fallback.py.
  • Delete the copy inside your current project folder.
  • Confirm that claude_fallback.py no longer appears in the current project folder.

Nice Work!

Nice Work!

Mission accomplished! Your reusable Python client now recovers from overloaded Claude routes on its own.

You've learned how to:

  • Performed a 529 overload diagnosis with a local failing endpoint. The raw overloaded_error response proved that the failure came from model capacity.
  • Built a bounded retry strategy with exponential backoff. Added random jitter to spread repeated calls. Logged every attempt with its wait time.
  • Configured provider and model fallback through OpenRouter. Ran ten calls in a loop. Printed a summary table showing the answering route for each call. Confirmed the serving providers on the Activity page.
  • Secret Mission: Applied the same resilience pattern in an optional extension to strengthen the reusable client.

Ready to quiz yourself?