跳到正文
RadarAI· OpenRouter:Announcements(RSS)·· 10 小时前AI 评分62

OpenRouter 实测教程:三种方法修复 OpenAI 与 Claude 的 429 限流错误

AI 导读

OpenRouter 发布教程,讲解如何修复 OpenAI 与 Claude API 的 429 限流错误,先读取 error.metadata.limit_source 判断是谁的限制,再对症处理。

正文 · AI 翻译

OpenRouter:公告·2026-11-11 08:00· 1 小时前AI 评分65

LLM API 限流:如何修复 OpenAI 和 Claude 的 429 错误

AI 导读

OpenRouter 发布教程,讲解如何修复 OpenAI 与 Claude API 的 429 限流错误,先读取 error.metadata.limit_source 判断是谁的限制,再对症处理。

正文 · 原文

429 Too Many Requests 错误意味着你在当前时间窗口内发送的请求数或 token 数超过了限额。OpenAI、Anthropic 和 OpenRouter 都会返回该错误,它通常出现在应用首次承受真实负载,或批处理任务分散到多个 worker 上运行时。

大多数人会直接重试,而在默认设置下这往往不够。当我们通过 OpenAI Python SDK 针对每分钟限额运行一批 60 个提示词时,在 SDK 重试之后仍有 23 个请求失败。SDK 对每个请求放弃后 4 到 8 秒,窗口才重置。

本指南从一个真实的 429 出发,读取其响应头,并用三种方式修复:在错误给出的重置时间重试、将溢出请求发送到备用模型,以及让另一个提供商提供同一模型。我们在 OpenRouter API 上运行了每个示例,每个代码块下方的输出就是我们的实际结果。

简要说明

  • 429 表示你在当前窗口内触发了限额。在 OpenRouter 上,error.metadata.limit_source 会告诉你这是谁的限额。

  • 在默认设置下,OpenAI Python SDK 对每个 429 重试两次,并在约 2 秒内放弃。针对每分钟限额,我们这批 60 个请求中仍有 23 个失败。

  • 等待 Retry-After 或 X-RateLimit-Reset 的重试在每次运行中都完成了全部 60 个请求,Python 耗时 89 到 92 秒,TypeScript 耗时 106 到 218 秒。

  • 模型回退在 13 秒内完成了全部 60 个请求,花费 $0.000066,无需等待。

  • 在固定单一提供商并关闭故障转移的情况下,每次运行最多有 150 个请求中的 32 个失败。开启故障转移后,每次运行中全部 150 个请求都成功。

429 限流错误是什么样子?

我们一次性向 poolside/laguna-s-2.1:free 发送了 40 个请求,这是一个每分钟限额 20 个请求的免费模型。38 个成功,2 个由提供该模型的提供商返回 429:

200: 38  429: 2  other: 0
429 limit_source: {'upstream_provider_shared_pool': 2}

来自我们自身免费层限额的 429 带有不同的 limit_source。以下是另一次运行中的一个例子:

{
  "message": "Rate limit exceeded: free-models-per-min. ",
  "code": 429,
  "metadata": {
    "headers": {
      "X-RateLimit-Limit": "20",
      "X-RateLimit-Remaining": "0",
      "X-RateLimit-Reset": "1792448640000"
    },
    "limit_source": "openrouter_free_tier_per_minute",
    "remedy_hint": "Slow down requests to free models, or retry after the per-minute window resets.",
    "provider_name": null
  }
}

响应头中携带了你需要的数字。X-RateLimit-Limit 是上限,X-RateLimit-Remaining 是剩余额度,X-RateLimit-Reset 是重试能够成功的最早时间,为 Unix 毫秒时间戳。响应体还增加了 limit_source,说明你触发的是谁的限额,以及一段为阅读日志的人编写的 remedy_hint。在代码中根据 limit_source 分支处理,而不是根据提示文本。

OpenAI 和 Anthropic 以不同的名称发送相同的信息。OpenAI 的 x-ratelimit-reset-requests 是一个时长,例如 1s,而两家提供商都可以将 retry-after 作为秒数发送。

为什么限流会在不同的请求数下触发?

40 个请求中有 38 个通过了 20 的限额。限流器可以接受同时到达的请求,此时计数尚未跟上,而且大多数限流器不会在每分钟开始时干净地重置。Anthropic 使用持续补充的令牌桶。OpenAI 可能返回代码为 slow_down 的 429,即使你的流量增长过快但仍在每分钟限额之内。

为了解我们的免费模型限额如何计数,我们以不同模式又发送了 161 个请求。以下是其中三种:

测试该分钟内成功数
顺序执行,等待两分钟空闲后30
顺序执行,紧接着的那一分钟30
30 并发29

由此得出两点结论。在我们测试的每一分钟里,都有超过 20 个请求成功,所以应把 20 视为 429 可能开始出现的临界点,而非精确计数。当我们在窗口还剩 58 秒时于 poolside/laguna-s-2.1:free 上触发该限制,发往 minimax/minimax-m3.1-flash:free 的五个请求立即通过了,而 Laguna 仍处于阻塞状态。但这并不意味着限制是按模型计算的。OpenRouter 对一个账户下的所有免费模型应用同一个每分钟限流器,因此发往另一个免费模型的请求也可能收到同样的 429。

第 1 步:判断你触发了谁的限制

只有当限制按计划重置时,等待才能解决 429,所以在选择修复方案前先检查 limit_source:

limit_sourceStatus谁的限制如何修复
openrouter_free_tier_per_minute429我们的免费模型上限等待 X-RateLimit-Reset(第 2 步),或回退到付费模型(第 3 步)
upstream_provider_shared_pool429提供该模型服务的供应商换用另一家供应商(第 4 步)或回退模型(第 3 步)
openrouter_in_flight_budget402你的在途支出预算等待 Retry-After 后重试
openrouter_key_limit、openrouter_credits402你的密钥信用额度或账户余额提高密钥额度或充值

429 decision flow: read error.metadata.limit_source. openrouter_free_tier_per_minute (429): wait for X-RateLimit-Reset or fall back to a paid model. upstream_provider_shared_pool (429): let OpenRouter route to another provider or add a fallback model. openrouter_in_flight_budget (402): wait for Retry-After and retry. openrouter_key_limit or openrouter_credits (402): raise the key's limit or add credits.

上游 429 意味着提供该模型服务的供应商容量不足。供应商不会说明该容量何时释放,因此这些错误在我们的运行中不携带重置头。Gemma 4 31B 的免费版本返回了以下内容:

google/gemma-4-31b-it:free is temporarily rate-limited upstream. Please retry shortly, or add your own key to accumulate your rate limits.

同一请求以付费版本作为回退,使用了另一家供应商的容量,并在 DeepInfra 上以 $0.0000037 成功。

有些看似限流的错误其实与费用有关。OpenAI 和 Anthropic 在达到月度支出上限时都会返回 429,在上限重置之前重试无济于事。在 OpenRouter 上,信用问题则以 402 返回。

第 2 步:在错误给出的重置时间重试

OpenAI 和 Anthropic 的 Python SDK 默认会对 429 重试两次,采用从 0.5 秒开始、最长不超过 8 秒的短暂退避。当服务器发送 Retry-After 头时,它们会遵循该头。

我们的免费层 429 发送的是 X-RateLimit-Reset,而 SDK 不读取该头,因此两次重试都落在同一个已满的分钟里。以下是通过 OpenAI SDK(openai 3.26.1)以默认设置、每次 10 个地运行 60 条提示的批次:

openai 3.26.1 | max_retries default: 2
finished in 12s  (HTTP requests sent: 112)
succeeded: 37  RateLimitError after retries: 23
slowest failure gave up after 2.3s
message: Rate limit exceeded: free-models-per-min.  | limit_source: openrouter_free_tier_per_minute
  limit_source=openrouter_free_tier_per_minute  x-ratelimit-reset: 1792444980000  retry-after: None  window reset 8s after the SDK gave up
  limit_source=openrouter_free_tier_per_minute  x-ratelimit-reset: 1792444980000  retry-after: None  window reset 7s after the SDK gave up
  ...
  limit_source=openrouter_free_tier_per_minute  x-ratelimit-reset: 1792444980000  retry-after: None  window reset 4s after the SDK gave up

SDK 为 60 条提示发送了 112 个 HTTP 请求,仍有 23 个失败。提高 max_retries 最终能通过,但代价是每 8 秒就向一个已满的窗口发送请求,而且 OpenAI 的文档指出失败的请求仍会计入你的每分钟限额。更好的重试方式是等到服务器给出的时间。要运行全部 60 条提示,请使用当天至少还剩 60 次免费模型请求的账户,因为未购买 10 个额度的账户每天只有 50 次:

import email.utils
import os
import random
import time
from concurrent.futures import ThreadPoolExecutor

import requests

URL = "https://openrouter.ai/api/v1/chat/completions"
HEADERS = {"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}"}

def retry_delay(headers, attempt):
    """Seconds to wait after a 429: Retry-After, then X-RateLimit-Reset, then backoff."""
    retry_after = headers.get("Retry-After")
    if retry_after:
        if retry_after.replace(".", "", 1).isdigit():
            return float(retry_after)
        when = email.utils.parsedate_to_datetime(retry_after).timestamp()
        return max(0.0, when - time.time())
    reset = headers.get("X-RateLimit-Reset")  # Unix time in milliseconds
    if reset:
        # Spread clients over a few seconds after the reset, so they don't all retry at once.
        return max(0.0, int(reset) / 1000 - time.time()) + random.uniform(1, 5)
    return min(2 ** attempt, 60) * random.uniform(0.5, 1)  # exponential backoff with jitter

def chat(body, max_attempts=6):
    for attempt in range(max_attempts):
        r = requests.post(URL, headers=HEADERS, json=body, timeout=120)
        source = None
        if r.status_code in (402, 429):
            source = r.json()["error"].get("metadata", {}).get("limit_source")
        if source == "openrouter_free_tier_daily":  # resets at UTC midnight, so don't sleep until then
            r.raise_for_status()
        # A 402 from the in-flight budget clears after Retry-After; other 402s need credits.
        if r.status_code != 429 and source != "openrouter_in_flight_budget":
            r.raise_for_status()
            return r.json()
        if attempt == max_attempts - 1:
            break  # out of attempts, so don't wait before failing
        wait = retry_delay(r.headers, attempt)
        print(f"{r.status_code} ({source}), attempt {attempt + 1}: waiting {wait:.1f}s")
        time.sleep(wait)
    r.raise_for_status()

if __name__ == "__main__":
    def job(i):
        return chat({
            "model": "poolside/laguna-s-2.1:free",
            "messages": [{"role": "user", "content": f"Reply with the number {i}."}],
            "max_tokens": 16,
        })

    t0 = time.time()
    with ThreadPoolExecutor(max_workers=10) as pool:  # the same batch job: 60 prompts, 10 at a time
        results = list(pool.map(job, range(60)))
    print(f"{len(results)} of 60 completed in {time.time() - t0:.0f}s")
429 (openrouter_free_tier_per_minute), attempt 1: waiting 19.3s
429 (openrouter_free_tier_per_minute), attempt 1: waiting 19.1s
429 (openrouter_free_tier_per_minute), attempt 1: waiting 18.8s
429 (openrouter_free_tier_per_minute), attempt 1: waiting 17.2s
429 (openrouter_free_tier_per_minute), attempt 1: waiting 19.7s
429 (openrouter_free_tier_per_minute), attempt 1: waiting 15.6s
429 (openrouter_free_tier_per_minute), attempt 1: waiting 14.8s
429 (openrouter_free_tier_per_minute), attempt 1: waiting 16.4s
429 (openrouter_free_tier_per_minute), attempt 2: waiting 61.7s
429 (openrouter_free_tier_per_minute), attempt 2: waiting 58.9s
429 (openrouter_free_tier_per_minute), attempt 2: waiting 60.1s
429 (openrouter_free_tier_per_minute), attempt 2: waiting 57.8s
60 of 60 completed in 89s

全部 60 个请求都完成了,但该批次耗时 89 秒而非 12 秒。这种方法不会失败,每个触及限制的请求都会等待窗口结束。

retry_delay 优先检查 Retry-After,因为它是服务器的直接指令,而 X-RateLimit-Reset 只标记窗口何时滚动。随机的 1 到 5 秒可避免 10 个工作线程在同一瞬间重试并立刻填满新窗口。

即便如此,本次运行中有四个请求在新窗口已经填满之后才重试,因此它们又等了一轮。如果你像这样把 SDK 调用包在循环里,请在客户端设置 max_retries=0,否则你的每次尝试都会变成三个请求。

TypeScript 中的相同重试

retry.ts 遵循同样的顺序:先 Retry-After,再 X-RateLimit-Reset,最后退避。它使用内置的 fetch,因此不需要任何包。我们在 Node 24 上用 npx tsx retry.ts 运行了它:

const URL = "https://openrouter.ai/api/v1/chat/completions";
const HEADERS = {
  Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
  "Content-Type": "application/json",
};

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const randomBetween = (min: number, max: number) => min + Math.random() * (max - min);

/** Seconds to wait after a 429: Retry-After, then X-RateLimit-Reset, then backoff. */
export function retryDelay(headers: Headers, attempt: number): number {
  const retryAfter = headers.get("Retry-After");
  if (retryAfter) {
    const seconds = Number(retryAfter);
    if (!Number.isNaN(seconds)) return seconds;
    const when = Date.parse(retryAfter); // HTTP date
    if (!Number.isNaN(when)) return Math.max(0, (when - Date.now()) / 1000);
  }
  const reset = headers.get("X-RateLimit-Reset"); // Unix time in milliseconds
  if (reset) {
    // Spread clients over a few seconds after the reset, so they don't all retry at once.
    return Math.max(0, (Number(reset) - Date.now()) / 1000) + randomBetween(1, 5);
  }
  return Math.min(2 ** attempt, 60) * randomBetween(0.5, 1); // exponential backoff with jitter
}

export async function chat(body: object, maxAttempts = 6): Promise<any> {
  let res: Response | undefined;
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    res = await fetch(URL, { method: "POST", headers: HEADERS, body: JSON.stringify(body) });
    if (res.status !== 402 && res.status !== 429) {
      if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
      return res.json();
    }
    const error = (await res.json()).error;
    const source = error?.metadata?.limit_source;
    // A 402 from the in-flight budget clears after Retry-After; other 402s need credits.
    if (res.status === 402 && source !== "openrouter_in_flight_budget") {
      throw new Error(`HTTP 402: ${JSON.stringify(error)}`);
    }
    if (source === "openrouter_free_tier_daily") {
      throw new Error(`HTTP 429: ${JSON.stringify(error)}`); // resets at UTC midnight, so don't sleep until then
    }
    if (attempt === maxAttempts - 1) break; // out of attempts, so don't wait before failing
    const wait = retryDelay(res.headers, attempt);
    console.log(`${res.status} (${source}), attempt ${attempt + 1}: waiting ${wait.toFixed(1)}s`);
    await sleep(wait * 1000);
  }
  throw new Error(`Still rate limited after ${maxAttempts} attempts`);
}

// The same batch job: 60 prompts, 10 at a time.
async function main() {
  const prompts = Array.from({ length: 60 }, (_, i) => i);
  const results: any[] = [];
  const worker = async () => {
    for (let i = prompts.shift(); i !== undefined; i = prompts.shift()) {
      results[i] = await chat({
        model: "poolside/laguna-s-2.1:free",
        messages: [{ role: "user", content: `Reply with the number ${i}.` }],
        max_tokens: 16,
      });
    }
  };

  const t0 = Date.now();
  await Promise.all(Array.from({ length: 10 }, worker));
  const completed = results.filter(Boolean).length;
  console.log(`${completed} of 60 completed in ${Math.round((Date.now() - t0) / 1000)}s`);
}

main();
429 (openrouter_free_tier_per_minute), attempt 1: waiting 34.9s
429 (openrouter_free_tier_per_minute), attempt 1: waiting 34.6s
429 (openrouter_free_tier_per_minute), attempt 1: waiting 35.1s
429 (openrouter_free_tier_per_minute), attempt 1: waiting 33.5s
429 (openrouter_free_tier_per_minute), attempt 2: waiting 60.5s
429 (openrouter_free_tier_per_minute), attempt 2: waiting 58.6s
429 (openrouter_free_tier_per_minute), attempt 2: waiting 60.6s
60 of 60 completed in 106s

总耗时取决于批次开始时窗口所处的位置,以及模型提供方的繁忙程度。我们的两次 TypeScript 运行分别耗时 218 秒和 106 秒,两次 Python 运行分别耗时 92 秒和 89 秒。四次运行都完成了每个请求。

第 3 步:将溢出请求发送到回退模型

如果第二个模型的回答可以接受,你根本不需要等待。提供方的速率限制只作用于某个提供方上的某个模型,因此不同的模型使用独立的容量。免费层上限是例外,因为你账户上的每个 :free 模型都共享该上限。使用 models 参数时,我们会按顺序尝试每个模型,并在出现任何错误(包括 429)时转到下一个模型,在请求到达你的代码之前:

import os
import time
from collections import Counter
from concurrent.futures import ThreadPoolExecutor

import requests

URL = "https://openrouter.ai/api/v1/chat/completions"
HEADERS = {"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}"}

def job(i):
    r = requests.post(URL, headers=HEADERS, timeout=120, json={
        # Try the free model first; if it is rate limited, OpenRouter sends the request to the next one.
        "models": ["poolside/laguna-s-2.1:free", "deepseek/deepseek-v4.1-flash"],
        "messages": [{"role": "user", "content": f"Reply with the number {i}."}],
        "max_tokens": 16,
    })
    data = r.json()
    return r.status_code, data.get("model"), data.get("usage", {}).get("cost", 0)

t0 = time.time()
with ThreadPoolExecutor(max_workers=10) as pool:  # the same batch job: 60 prompts, 10 at a time
    results = list(pool.map(job, range(60)))
print(f"finished in {time.time() - t0:.0f}s")
print(Counter((status, model) for status, model, _ in results))
print(f"total cost: ${sum(cost for *_, cost in results):.6f}")
finished in 13s
Counter({(200, 'poolside/laguna-s-2.1:free'): 52, (200, 'deepseek/deepseek-v4.1-flash'): 8})
total cost: $0.000066

免费模型处理了 52 个请求,付费模型处理了另外 8 个。没有任何请求等待,你只需为付费的请求付费。响应的 model 字段记录了哪个模型作出了回答,也就是你被计费的那个模型。

选择一个拥有独立容量的回退模型。你账户上的每个 :free 模型都共享免费模型上限,因此当该上限是原因时,另一个免费模型并不是可靠的回退。免费模型也经常耗尽提供方容量,因此付费回退是更可靠的选择。

以下是同一批次的三个运行结果并排对比:

运行完成数时间成本
OpenAI SDK,默认重试60 个中的 37 个12 秒$0
等待重置的重试60 个中的 60 个89 秒$0
模型回退60 个中的 60 个13 秒$0.000066

如果任务可以等待,就在重置时重试。如果不能等待,并且第二个模型的回答足够好,就添加回退。

第 4 步:让另一个提供方服务同一个模型

第 2 步和第 3 步处理我们的免费模型上限。付费模型会遇到另一种限制:正在服务它们的提供方的容量。OpenRouter 上的大多数模型由多个提供方服务,每个提供方都有自己的速率限制。当一个提供方返回 429 时,我们会在错误到达你之前,在下一个提供方重试该请求。此功能默认开启。

我们向 google/gemma-4-31b-it 发送了 300 个请求,每次 50 个,并检查了每个请求的 GET /api/v1/generation 记录中的 provider_responses 列表,该列表显示了我们尝试过的每个提供方:

300 requests, 50 at a time, in 11s
status codes: Counter({200: 300})
generation records read: 300 of 300
served by: {'CoreWeave': 177, 'DeepInfra': 118, 'Novita': 1, 'Venice': 1, 'ModelRun': 1, 'Parasail': 2}
requests that needed more than one provider: 0
failed attempts, by provider and status: []
total cost: $0.00130

分散到六个提供方,没有任何请求需要第二次尝试。然后我们用 “provider”: {“order”: [“crusoe”]} 将同一模型固定到 Crusoe,并发送了 150 个请求,每次 50 个。以下是开启回退时的一次运行:

150 requests, 50 at a time, in 5s
status codes: Counter({200: 150})
generation records read: 150 of 150
served by: {'CoreWeave': 79, 'Crusoe': 71}
requests that needed more than one provider: 79
failed attempts, by provider and status: [(('Crusoe', 429), 79)]
total cost: $0.00056

Crusoe 对 79 个请求进行了速率限制,CoreWeave 回答了全部 79 个,客户端看到了 150 次成功。我们在关闭和开启 allow_fallbacks 的情况下多次重复了固定提供方的运行:

allow_fallbacks运行次数每次运行成功数每次运行 429 数
false4150, 147, 118, 1360, 3, 32, 14
true3150, 150, 1500

Crusoe 的容量每分钟都在变化,因此在关闭回退的情况下,失败次数从零到 32 不等。开启回退后,Crusoe 在三次运行中分别对 21、41 和 79 个请求进行了限流,而客户端从未看到其中任何一个。任何缩小可服务请求的提供商范围的做法,都会减少吸收突发流量的容量。如果你在 OpenRouter 上仍然看到提供商的 429,请检查是否存在 allow_fallbacks: false、过短的 only 列表或过紧的 max_price。若想优先使用某个提供商又不失去故障转移,请设置 order 并将 allow_fallbacks 保持默认值。

同样向 anthropic/claude-haiku-5.5 和 openai/gpt-6-luna 发送 300 个请求的突发流量,没有出现失败的尝试。当闭源模型自身的 API 容量耗尽时,错误通常是 529 而不是 429,Claude 529 过载指南对此有介绍。

如何完全避免触及速率限制?

上述修复方法都是在 429 发生之后进行处理。以下习惯可以降低其发生的可能性:

  • 将并发数限制在限额允许的范围内。并发正是导致我们第一次测试超限的原因。将请求节奏控制在略低于限额的水平,比在 429 之后等待整个窗口结束所损失的时间更少。

  • 将 max_tokens 设置得接近你预期的答案长度。OpenAI 会将 max_tokens 计入你的每分钟令牌限额,因此过于宽裕的取值会比答案本身更快地耗尽限额。

  • 在大型任务前检查你的密钥。GET /api/v1/key 会报告每日免费模型计数器。在这些运行之后,我们的读数为 {'used': 585, 'limit': 1000, 'remaining': 415}。在你购买至少 10 个积分之前,每日限额为 50 个免费模型请求,之后为 1,000 个。

  • 在生产环境中使用付费变体。每分钟 20 次的上限仅适用于 :free 模型。

常见问题

如何提高我的 OpenAI API 速率限制?

当你的组织累计积分购买量超过每个使用层级阈值时,OpenAI 会自动提高你的限额。你可以在 OpenAI 仪表盘的 Settings > Organization > Limits 下查看当前限额。通过 OpenRouter,提供商路由会将模型的流量分散到所有为其提供服务的提供商,因此单个提供商的限额不会限制你。

为什么 Claude 提示“rate limit reached”?

在 Claude API 上,这意味着你的组织已用完该模型的每分钟请求数或令牌数,retry-after 头会说明需要等待多少秒。如果 429 来自你的月度支出上限,则没有 retry-after 头,在访问恢复之前重试也无济于事。在订阅版 Claude Code 中,该消息通常意味着套餐的使用限额。参见触及 Claude Code 使用限额?。

更多的 API 密钥能绕过速率限制吗?

不能。在 OpenRouter 上,额外的 API 密钥或账户不会改变你的速率限制,因为我们在全局范围内管理容量。提供商的限额按模型适用,这就是回退模型有效的原因,但你账户上的每个 :free 模型共享一个免费层级上限。使用 BYOK 时,使用你自己提供商密钥的请求会计入你在该提供商处的限额。

Retry-After 的单位是秒还是毫秒?

秒,或一个 HTTP 日期。我们的 X-RateLimit-Reset 不同,它是毫秒级的 Unix 时间戳。第 2 步中的 retry_delay 函数会处理所有三种形式。

后续步骤

  • 限额:免费模型上限、X-RateLimit-* 头以及如何处理 402 和 429 错误。

  • 提供商路由:order、allow_fallbacks、only 和排序。

  • 模型回退:models 参数以及回退请求如何计费。

  • 免费 LLM API 对比:各提供商的免费额度限制。

来源:OpenRouter:Announcements · openrouter.ai

来源:RadarAI · aihot.news