OpenRouter 教程:如何修复 OpenAI 与 Claude 的 429 限流错误
LLM API Rate Limits: How to Fix OpenAI and Claude 429 Errors
OpenRouter 发布了教程,讲解如何读取 429 响应头并按 limit_source 判断限流来源,给出了重试、模型回退、供应商故障转移三种修复方式。
作者用同一批 60 条请求实测三种 429 处理方式,给出耗时与成本对比,可直接迁移到自己的重试逻辑。
A 429 错误表示您发送的请求或令牌超过了当前时间窗口内允许的限制。OpenAI、Anthropic 和 OpenRouter 都会返回此错误,通常在应用程序在真实负载下运行或批量作业扩展到多个工作进程时首次出现。
大多数人会尝试重试,但在默认设置下,这通常不够。当我们通过 OpenAI Python SDK 对每分钟的限制运行一个包含 60 个提示的批量作业时,SDK 重试后仍有 23 个请求失败。错误重置窗口在 SDK 放弃每个请求后的 4 到 8 秒内重置。
本指南从一个真实的 429 错误开始,读取其头部信息,并提供了三种解决方法:在错误给出的重置时间进行重试、将溢出请求发送到备用模型,以及让另一个提供者提供相同的模型。我们针对 OpenRouter API 运行了所有示例,每个代码块下的输出是我们得到的结果。
Tl;dr
-
A 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,无需等待。
-
在一个提供者上固定,且故障转移关闭时,每次运行最多有 32 个请求失败。开启故障转移后,每次运行所有 150 个请求都成功。
429 流量限制错误看起来是什么样子?
我们一次性发送了 40 个请求到 poolside/laguna-s-2.1:free,这是一个免费模型,每分钟限制为 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,它说明了您达到了谁的限制,并写有针对阅读日志的人的提示。在您的代码中根据 limit_source 分支,而不是提示文本。
OpenAI 和 Anthropic 在不同的名称下发送相同的信息。OpenAI 的 x-ratelimit-reset-requests 是一个持续时间,例如 1s,并且 两个提供者 都可以以秒数为单位发送 retry-after。
为什么流量限制会在不同的请求计数时触发?
40 个请求中有 38 个通过了每分钟 20 个请求的限制。限制器可以在计数更新之前接受同时到达的请求,并且大多数限制器不会在每个小时的顶部干净地重置。Anthropic 使用一个持续填充的令牌桶。OpenAI 可以在您的流量增加过快时返回 429,即使您仍在每分钟限制之下。
为了查看我们的免费模型限制如何计数,我们以不同的模式发送了 161 个更多请求。以下是其中的三种:
| 测试 | 该分钟内成功 |
|---|---|
| 顺序的,经过两个空闲分钟 | 30 |
| 顺序的,紧接着的分钟 | 30 |
| 30 个并发 | 29 |
从中产生了两件事。在测试的每一分钟里,超过20个请求成功,所以应该把20当作429错误开始的点,而不是一个确切的数字。当我们在poolside/laguna-s-2.1:free上,窗口还剩58秒时遇到它,五个请求到minimax/minimax-m3.1-flash:free立即通过,而Laguna仍然被阻止。但这并不是每个模型的限制。OpenRouter对所有账户上的免费模型应用每分钟一个限制器,所以一个到不同免费模型的请求可以得到相同的429。
步骤1:阅读你遇到的限制是谁的
等待只有在限制按计划重置时才会修复429错误,所以在选择修复方法之前,请检查limit_source:
| limit_source | 状态 | 是谁的限制 | 什么修复它 |
|---|---|---|---|
| openrouter_free_tier_per_minute | 429 | 我们的免费模型限制 | 等待X-RateLimit-Reset(步骤2),或者切换到付费模型(步骤3) |
| upstream_provider_shared_pool | 429 | 提供该模型的供应商 | 另一个供应商(步骤4)或一个备用模型(步骤3) |
| openrouter_in_flight_budget | 402 | 你的飞行中支出预算 | 等待Retry-After并重试 |
| openrouter_key_limit, openrouter_credits | 402 | 你的密钥的信用限制或你的余额 | 提高密钥的限制或添加信用 |

上游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,SDKs不读取它,所以两次重试都落在了同一个完整的分钟里。这是通过OpenAI SDK(openai 3.26.1)使用默认设置进行的60个提示批量,每次10个:
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 upSDK为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,然后 backoff。它使用内置的 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)移动到下一个,在 anything 到达您的代码之前:
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,默认重试 | 37 of 60 | 12 s | $0 |
| 等待重置的重试 | 60 of 60 | 89 s | $0 |
| 模型备用 | 60 of 60 | 13 s | $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分布在六个提供者之间,没有请求需要第二次尝试。然后我们将相同的模型固定到 Crusoe,使用 “provider”:{“order”:[“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.00056Crusoe 限制了 79 个请求,CoreWeave 回答了所有 79 个,客户端看到了 150 个成功。我们多次重复固定运行,允许备用开关:
| allow_fallbacks | 运行 | 每次运行成功 | 每次运行 429 |
|---|---|---|---|
| false | 4 | 150, 147, 118, 136 | 0, 3, 32, 14 |
| true | 3 | 150, 150, 150 | 0 |
Crusoe 的容量每分钟都在变化,所以禁用备用时的失败从没有到 32 不等。启用备用时,三个运行中 Crusoe 限制了 21、41 和 79 个请求,客户端从未看到其中任何一个。任何缩小可以服务请求的提供者的选择都会减少吸收突发的能力。如果您在 OpenRouter 上仍然看到提供者 429,请检查 allow_fallbacks:false、一个短列表或一个严格的 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 仪表板的设置 > 组织 > 限制下查看你当前的限制。通过 OpenRouter,提供商路由将模型的流量分配到为它提供服务的每个提供商,所以一个提供商的限制不会限制你。
为什么 Claude 会说“速率限制达到”?
在 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 错误。
-
提供商路由:顺序、allow_fallbacks、only 和排序。
-
模型备用:模型参数以及备用请求如何计费。
-
免费 LLM API 对比:各提供商免费层级的限制。
来源: OpenRouter Blog · openrouter.ai