Invite & Earn

How invite rewards work

Share your invite link. When a friend registers through it and tops up, you receive the displayed reward on their subsequent top-ups.

OpenRouter Rate Limits and HTTP 429: How to Diagnose and Recover

A practical guide to diagnosing HTTP 429 and 402 errors in OpenRouter: distinguishing free tier quotas from platform and upstream provider limits, inspecting headers and metadata, and implementing a resilient Python retry script.

Contents
OpenRouter Rate Limits and HTTP 429: How to Diagnose and Recover

When sending high volumes of requests to AI models through the OpenRouter gateway, client applications frequently encounter HTTP 429 Too Many Requests status codes. Because OpenRouter aggregates dozens of independent inference providers, this error can originate at completely different tiers of the infrastructure. A naive immediate retry often leads to client rate-limiting or wasted retry attempts. Restoring stable request flow requires identifying the exact layer of failure and choosing the matching remediation: backing off, reducing concurrency, switching model providers, or adjusting spending caps.

Tiers of Limitations: Free Tier, Platform, and Upstream

The official OpenRouter Limits documentation distinguishes between service-wide rate limits, individual provider throughput, and account balance enforcement.

On the OpenRouter Pricing page, the base free tier enforces a ceiling of 50 requests per day for publicly available free models (those carrying the :free suffix). Because exact requests-per-minute (RPM) limits and tier thresholds can change over time, live numerical values should always be verified against the live limits table.

When diagnosing errors, it is critical to separate the layers responsible for returning HTTP 429 and the related HTTP 402 status:

  1. OpenRouter platform rate limits. These occur when the router itself receives requests too rapidly. The daily free pool quota belongs to this platform-level limitation category (rather than an independent third-party restriction): when exceeding the 50 requests per day limit on free models, the platform rejects incoming requests until the daily counter resets. When a platform-level rate limit is triggered, the server returns the X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset HTTP headers. Successful responses (HTTP 200) do not include these operational headers, meaning client applications cannot rely on them during normal, non-error traffic to predict limits.
  2. Upstream provider rate limits. Models are physically hosted and run on the infrastructure of specific upstream companies (such as Anthropic, Meta, DeepSeek, Mistral, or specialized third-party cloud hosts). If that upstream partner’s infrastructure is overloaded, OpenRouter propagates the 429 status code back to the client. In the response body structure, the error.metadata.provider_code field contains the upstream provider’s raw error code when available (for example, 429), rather than the provider’s name or string identifier.
  3. Financial constraints (HTTP 402 Payment Required). The OpenRouter Limits documentation explicitly differentiates billing depletion from frequency rate limits. A 402 status code indicates an insufficient or negative account balance, or that an individual API key spending cap has been reached, rather than strictly representing a zero account balance.

Current key parameters can be inspected with a direct API request:

curl -s -X GET https://openrouter.ai/api/v1/key \
  -H "Authorization: Bearer $OPENROUTER_API_KEY"

The response payload returns the usage, limit_reset, and limit_remaining fields. A value of limit_remaining: null indicates that no local spending cap is enforced on this specific API key. This value does not verify whether credits exist on the organization’s primary balance; it merely confirms that no artificial spending cap has been configured for this individual token.

Diagnostic Summary Table

Response Code & SignalsInspection SourceRoot CauseClient Action
HTTP 402 Payment RequiredGET /api/v1/key endpoint or dashboardInsufficient/negative organization balance or key spending cap reached (limit_remaining: 0)Top up balance or raise key limit; programmatic retries without changes are useless
HTTP 429 with X-RateLimit-* headersGateway HTTP response headersExceeded OpenRouter platform concurrency or request frequency limitsInspect Retry-After header and reduce the number of concurrent threads
HTTP 429 with code in provider_codeerror.metadata.provider_code JSON field (optional)Upstream provider overload or outage (upstream’s raw error code)Switching models or providers may help but does not guarantee recovery; attribute specific provider in Activity > provider_responses
HTTP 429 on :free modelsOpenRouter Pricing sectionExhausted daily platform limit (50 requests/day) or overall capacityUpgrade to a paid model or defer execution
HTTP 429 with complex routingDashboard: Activity > Request > View Raw MetadataIntermediate node failure inside provider_responses objectAttribute failing provider and verify fallback chain in the BYOK/Routing guide

Analyzing Two Scenarios: Key Cap vs. Provider Failure

How a client application responds when requests fail depends on metadata inspection. Below are two hypothetical scenarios (illustrative examples rather than observations from a real account).

Scenario 1 (Hypothetical): Local Token Limit Depletion

In this hypothetical scenario, a background worker receives an HTTP 402 rejection. A positive organization balance is an explicit baseline assumption here, verified separately via the web dashboard (the key endpoint response itself does not confirm organization-wide balance). Querying the https://openrouter.ai/api/v1/key endpoint returns:

{
  "data": {
    "label": "worker-key",
    "usage": 25.04,
    "limit": 25.0,
    "is_free_tier": false,
    "limit_remaining": 0.0,
    "limit_reset": null
  }
}

While the primary account balance is positive by premise, the limit_remaining field has reached zero. The token has hit its administrator-configured spending limit of $25. Any automated retries using this key will repeatedly fail with the same 402 error. The worker process should terminate immediately and alert an administrator to adjust the key’s spending cap.

Scenario 2 (Hypothetical): Upstream Provider Overload

As an illustrative example, consider a case where an incoming request returns HTTP 429 due to an upstream outage. Receiving a 429 status code provides no definitive insight into overall gateway health or remaining account balance. The error response body may include an optional metadata block:

{
  "error": {
    "message": "Provider returned rate limit error",
    "code": 429,
    "metadata": {
      "provider_code": 429
    }
  }
}

The error.metadata.provider_code field is optional and conveys the raw upstream provider status code when available (in this example, 429)—not the provider’s name or string identifier. The presence of this code alone does not reveal which specific host rejected the call.

To identify the failing provider, navigate in the management dashboard to: Activity > specific request > View Raw Metadata. The provider_responses object lists each evaluated provider along with its returned status, as documented in the routing guide. While switching to another provider or changing the target model may resolve the issue, it does not guarantee immediate recovery.

Python 3 Client Retry Script

When an HTTP 429 status is transient, the delay before the next attempt is calculated using the Retry-After header. The server conveys this header either as an integer number of seconds or as an HTTP-formatted date string.

The following implementation relies exclusively on the Python 3 standard library. It handles only HTTP 429 errors, applies exponential backoff with random jitter when a server-provided delay header is missing, and aborts execution if the server requests a backoff period exceeding 60 seconds.

import email.utils
import json
import os
import random
import sys
import time
import urllib.error
import urllib.request

API_KEY = os.environ.get("OPENROUTER_API_KEY")
MODEL_ID = os.environ.get("OPENROUTER_MODEL_ID", "openai/gpt-4o-mini")
MAX_ATTEMPTS = 3
MAX_ACCEPTABLE_WAIT = 60.0


def parse_retry_after(header_value: str | None) -> float | None:
    if not header_value:
        return None
    raw = header_value.strip()
    if raw.isdigit():
        return max(0.0, float(raw))
    try:
        parsed_date = email.utils.parsedate_to_datetime(raw)
        delay = parsed_date.timestamp() - time.time()
        return max(0.0, delay)
    except Exception:
        return None


def execute_completion(prompt_text: str) -> str | None:
    if not API_KEY:
        sys.stderr.write("Переменная окружения OPENROUTER_API_KEY не задана.\n")
        return None

    endpoint = "https://openrouter.ai/api/v1/chat/completions"
    payload = json.dumps(
        {
            "model": MODEL_ID,
            "messages": [{"role": "user", "content": prompt_text}],
        }
    ).encode("utf-8")

    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    }

    for attempt in range(1, MAX_ATTEMPTS + 1):
        req = urllib.request.Request(endpoint, data=payload, headers=headers, method="POST")
        try:
            with urllib.request.urlopen(req, timeout=30) as response:
                status_code = response.getcode()
                body = response.read().decode("utf-8")
                if status_code == 200:
                    data = json.loads(body)
                    return data["choices"][0]["message"]["content"]
        except urllib.error.HTTPError as err:
            if err.code == 429:
                retry_header = err.headers.get("Retry-After")
                server_delay = parse_retry_after(retry_header)

                if server_delay is not None:
                    wait_seconds = server_delay
                else:
                    base_delay = 2.0 ** attempt
                    wait_seconds = base_delay + random.uniform(0.1, 1.0)

                if wait_seconds > MAX_ACCEPTABLE_WAIT:
                    sys.stderr.write(
                        f"Сервер запросил паузу {wait_seconds:.1f} с. "
                        "Ожидание превышает 60 секунд. Запрос отменен.\n"
                    )
                    return None

                if attempt == MAX_ATTEMPTS:
                    sys.stderr.write("Исчерпан лимит из 3 попыток на статус 429.\n")
                    return None

                sys.stderr.write(
                    f"Получен 429. Попытка {attempt} завершилась неудачей. "
                    f"Пауза {wait_seconds:.2f} с перед следующим запросом.\n"
                )
                time.sleep(wait_seconds)
                continue
            elif err.code == 402:
                sys.stderr.write("Ошибка 402: проверьте баланс счета и лимит ключа.\n")
                return None
            else:
                sys.stderr.write(f"HTTP-ошибка {err.code}: запрос отклонен без повтора.\n")
                return None
        except urllib.error.URLError as err:
            sys.stderr.write(f"Сетевой сбой: {err.reason}. Повтор отменен.\n")
            return None

    return None


if __name__ == "__main__":
    result = execute_completion("Назови три базовых принципа надежности сетевых API.")
    if result:
        print(result)

Because the script snippet above preserves its original Russian diagnostic logging strings byte-for-byte, here is an explanation of its internal decision tree and localized equivalents:

  • Environment variable OPENROUTER_API_KEY is not set: Indicates that the OPENROUTER_API_KEY environment variable is missing; the function immediately halts without making network calls.
  • Server requested a pause ... Wait exceeds 60 seconds. Request canceled: Triggered when the server’s Retry-After header requires waiting longer than MAX_ACCEPTABLE_WAIT (60 seconds); the execution cancels immediately rather than blocking worker processes indefinitely.
  • Exhausted limit of 3 attempts on status 429: Indicates that all 3 retry attempts have been exhausted on HTTP 429 responses.
  • Received 429. Attempt X failed. Pause Y s before next request: Logs an interim 429 rate limit failure on attempt X and sleeps for the calculated backoff period Y before retrying.
  • Error 402: check account balance and key limit: Logs an unrecoverable HTTP 402 payment error indicating that the account balance is depleted or the key cap has been reached; no retries are attempted.
  • HTTP error {code}: request rejected without retry: Logs any other HTTP status code and terminates without retrying.
  • Network failure: {reason}. Retry canceled: Catches general socket or network errors (URLError) and halts execution to avoid re-issuing calls when the receipt of the previous payload is unknown.
  • The test prompt translates to “Name three basic principles of network API reliability.”

Retrying Network Requests and Side Effects

When designing tool-calling workflows within autonomous agent loops, retrying HTTP requests requires extreme caution. If a model has already invoked an external tool that modified external state on a prior turn (such as writing to a database, initiating a payment, or opening a support ticket), blindly repeating the chain risks duplicate execution. Requests must never be retried automatically if related business tools have already executed, or if the network returned an ambiguous result (such as a connection drop or socket timeout where it is uncertain whether the server received and processed the prompt).

In the script above, retries are strictly restricted to requests explicitly rejected with an HTTP 429 status code. Crucially, text generation should never be assumed to be idempotent or cost-free: re-issuing calls consumes additional token quotas and spend limits, while the stochastic nature of model sampling means subsequent completions may produce different outputs. If an agent experiences a failure while executing an external command, system state should be synchronized via an action log before resuming interaction with the language model.

Building Resilient Routing Architectures

On the pricing page, the Free tier enforces a platform-level cap of 50 requests per day across free models. Crucially, the absence of platform rate limits highlighted in documentation applies to transitioning to paid models, not simply purchasing account credits—depositing funds does not automatically remove rate limits from :free endpoints. Exact terms and account-specific quotas should always be verified in the live limits table. Furthermore, using paid models does not entirely eliminate upstream provider cluster congestion (and while switching providers can help mitigate downtime, it does not guarantee immediate recovery).

To ensure production reliability, engineering teams typically combine multiple defensive strategies:

  • Specifying fallback models in OpenRouter’s models request parameter array, allowing the router to automatically redirect traffic to an alternate provider if the primary choice fails.
  • Enforcing concurrency limits on the client side using job queues, rate limiters, or token bucket throttles.
  • Maintaining an independent backup route via alternative multi-model APIs with compatible request schemas for mission-critical infrastructure, enabling traffic migration during extended gateway outages.

Ready to optimize your LLM workflow?

Join thousands of developers building faster, smarter, and more cost-effective AI applications with BetterToken.

Get Started for Free