DEV Community

Cover image for I Built a Fallback System for OpenRouter's Free API After It Silently Broke
Hamimelon2026
Hamimelon2026

Posted on

I Built a Fallback System for OpenRouter's Free API After It Silently Broke

The Script That Failed Without Warning

My nightly batch job had been running fine for months on OpenRouter's free API. Then it started throwing 429 errors, and nothing in my code had changed. The actual cause: the specific :free model I'd hardcoded had quietly stopped being offered for free. No warning, no deprecation notice — it just started failing.

Here's the fallback system I built afterward, and the mechanics of OpenRouter's free tier that would've saved me the debugging session if I'd understood them going in.

How the Free Tier Actually Works

Free models on OpenRouter carry a :free suffix and cost $0 per token, but the catalog rotates — providers add and pull free variants regularly, and a model that's free today isn't guaranteed to stay that way. Rate limits apply on top of that: 20 requests per minute on free models, and a daily cap that depends on your account history — 50 requests a day if you've never purchased credits, or 1,000 a day once you've bought at least $10 in credits at any point (that higher limit is permanent even if your balance drops back to zero later).

None of that is hidden — it's in OpenRouter's own API documentation — but it's easy to build against a single hardcoded model ID and forget the catalog isn't static.

Getting an OpenRouter API Key

pip install openai python-dotenv
Enter fullscreen mode Exit fullscreen mode

Sign up at openrouter.ai, no card required, and generate a key from the dashboard. The OpenRouter API is OpenAI-compatible, so your existing client library works with just a different base_url:

import os
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

client = OpenAI(
    api_key=os.getenv("OPENROUTER_API_KEY"),
    base_url="https://openrouter.ai/api/v1"
)
Enter fullscreen mode Exit fullscreen mode

The Broken Version

def get_summary(text):
    response = client.chat.completions.create(
        model="some-provider/some-model:free",
        messages=[{"role": "user", "content": f"Summarize: {text}"}],
    )
    return response.choices[0].message.content
Enter fullscreen mode Exit fullscreen mode

This works until the hardcoded model stops being free or gets removed from the catalog — at which point every call fails with no fallback, exactly what happened to me.

The Fallback Version

import time

FREE_MODEL_FALLBACKS = [
    "provider-a/model-x:free",
    "provider-b/model-y:free",
    "provider-c/model-z:free",
]

def get_summary(text, models=FREE_MODEL_FALLBACKS, max_retries_per_model=1):
    last_error = None
    for model in models:
        for attempt in range(max_retries_per_model):
            try:
                response = client.chat.completions.create(
                    model=model,
                    messages=[{"role": "user", "content": f"Summarize: {text}"}],
                )
                return response.choices[0].message.content
            except Exception as e:
                last_error = e
                if "429" in str(e):
                    time.sleep(2)  # brief pause on rate limit before moving on
                continue
    raise Exception(f"All fallback models failed. Last error: {last_error}")
Enter fullscreen mode Exit fullscreen mode


If the first model in the list fails — rate limited, deprecated, or removed from the free catalog entirely — it moves to the next before giving up. This is a five-line change from the broken version, not a rewrite, and it turned a script that failed silently into one that degrades gracefully.

Tracking Rate Limits Across Multiple Scripts

The rate limit that actually caught me wasn't the daily cap — it was the 20-requests-per-minute limit, hit because I had two separate scripts calling the same key around the same time without either one tracking the other's usage. A simple shared counter fixes this if you're running more than one process against the same key:

import time
from collections import deque

request_timestamps = deque()

def rate_limited_call(model, messages, max_per_minute=18):  # slight buffer under 20
    now = time.time()
    while request_timestamps and now - request_timestamps[0] > 60:
        request_timestamps.popleft()
    if len(request_timestamps) >= max_per_minute:
        sleep_time = 60 - (now - request_timestamps[0])
        time.sleep(max(sleep_time, 0))
    request_timestamps.append(time.time())
    return client.chat.completions.create(model=model, messages=messages)
Enter fullscreen mode Exit fullscreen mode


What I'd Check Before Building on This
Don't hardcode a single free model ID for anything you plan to run long-term — check OpenRouter's current free-model list before shipping, since the roster shifts month to month
Build the fallback list in from the start, not after your first silent failure — it's a small amount of code either way
Track rate limits across every process sharing a key, not just within a single script — this is what actually broke my setup, not the daily cap
Where This Left Me

Once I had the fallback logic working reliably, I also tested the same workload through RouteAI, mainly to compare pricing and model availability against what I'd gotten used to on OpenRouter's free tier — that's a decision worth making on your own project's volume and budget, not something either option deserves credit for by default. The fallback pattern above is useful regardless of which gateway or provider you're calling.

TL;DR: OpenRouter's free API models rotate and carry real rate limits (20/min, 50-1000/day depending on account history) — hardcoding a single :free model ID will eventually break silently. Fallback code and rate-limit tracking above turn that into a graceful degradation instead of a 2am debugging session.

Worth exploring if this is relevant to your stack: www.fastrouteai.com

Top comments (0)