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 秒。
-
models 回退在 13 秒内完成了全部 60 个请求,花费 $0.000066,无需等待。
-
固定到单一提供商并关闭故障转移时,每次运行最多有 150 个请求中的 32 个失败。开启故障转移后,每次运行中全部 150 个请求都成功。
429 限流错误长什么样?
我们一次性向 poolside/laguna-s-2.1:free 发送了 40 个请求,这是一个每分钟限额 20 个请求的免费模型。38 个成功,2 个由提供该模型的服务商返回了 429:
200: 38 429: 2 其他: 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":"放慢对免费模型的请求速度,或在每分钟窗口重置后重试。",
"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 可以返回 429并带有 slow_down 代码,即使你仍处于每分钟限制之下。
为了了解我们的免费模型限制如何计数,我们以不同模式又发送了 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_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 在上游暂时受到速率限制。请稍后重试,或添加你自己的密钥以累积你的速率限制。
同一请求以付费版本作为回退,使用了另一家提供商的容量,并在 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 默认值:2
在 12 秒内完成(发送的 HTTP 请求数:112)
成功:37 重试后 RateLimitError:23
最慢的失败在 2.3 秒后放弃
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 窗口在 SDK 放弃后 8 秒重置
limit_source=openrouter_free_tier_per_minute x-ratelimit-reset: 1792444980000 retry-after: None 窗口在 SDK 放弃后 7 秒重置
...
limit_source=openrouter_free_tier_per_minute x-ratelimit-reset: 1792444980000 retry-after: None 窗口在 SDK 放弃后 4 秒重置
SDK 为 60 条提示发送了 112 次 HTTP 请求,仍有 23 次失败。提高 max_retries 最终能通过,但代价是每 8 秒就往一个已满的窗口里发一次请求,而且OpenAI 的文档指出失败的请求仍然计入你的每分钟限额。更好的重试方式是等到服务器给出的时间。要跑完这 60 条提示,请使用一个当天至少还剩 60 次免费模型请求的账号,因为未购买 10 个额度的账号每天只有 50 次:
importemail.utils
importos
importrandom
importtime
fromconcurrent.futuresimportThreadPoolExecutor
importrequests
URL="https://openrouter.ai/api/v1/chat/completions"
HEADERS={"Authorization":f"Bearer{os.environ['OPENROUTER_API_KEY']}"}
defretry_delay(headers, attempt):
"""收到 429 后等待的秒数:先看 Retry-After,再看 X-RateLimit-Reset,最后用退避。"""
retry_after=headers.get("Retry-After")
ifretry_after:
ifretry_after.replace(".","",1).isdigit():
returnfloat(retry_after)
when=email.utils.parsedate_to_datetime(retry_after).timestamp()
returnmax(0.0, when-time.time())
reset=headers.get("X-RateLimit-Reset")# Unix 时间,单位毫秒
ifreset:
# 在重置后把客户端分散到几秒内,避免它们同时重试。
returnmax(0.0,int(reset)/1000-time.time())+random.uniform(1,5)
returnmin(2**attempt,60)*random.uniform(0.5,1)# 带抖动的指数退避
defchat(body, max_attempts=6):
forattemptinrange(max_attempts):
r=requests.post(URL,headers=HEADERS,json=body,timeout=120)
source=None
ifr.status_codein(402,429):
source=r.json()["error"].get("metadata", {}).get("limit_source")
ifsource=="openrouter_free_tier_daily":# 在 UTC 午夜重置,所以不要一直睡到那时
r.raise_for_status()
# 来自 in-flight 预算的 402 会在 Retry-After 后清除;其他 402 需要额度。
ifr.status_code!=429andsource!="openrouter_in_flight_budget":
r.raise_for_status()
returnr.json()
ifattempt==max_attempts-1:
break# 尝试次数用尽,所以失败前不再等待
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__":
defjob(i):
returnchat({
"model":"poolside/laguna-s-2.1:free",
"messages": [{"role":"user","content":f"Reply with the number{i}."}],
"max_tokens":16,
})
t0=time.time()
withThreadPoolExecutor(max_workers=10)aspool:# 同一个批处理任务:60 个提示词,每次 10 个
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),第 1 次尝试:等待 19.3 秒
429 (openrouter_free_tier_per_minute),第 1 次尝试:等待 19.1 秒
429 (openrouter_free_tier_per_minute),第 1 次尝试:等待 18.8 秒
429 (openrouter_free_tier_per_minute),第 1 次尝试:等待 17.2 秒
429 (openrouter_free_tier_per_minute),第 1 次尝试:等待 19.7 秒
429 (openrouter_free_tier_per_minute),第 1 次尝试:等待 15.6 秒
429 (openrouter_free_tier_per_minute),第 1 次尝试:等待 14.8 秒
429 (openrouter_free_tier_per_minute),第 1 次尝试:等待 16.4 秒
429 (openrouter_free_tier_per_minute),第 2 次尝试:等待 61.7 秒
429 (openrouter_free_tier_per_minute),第 2 次尝试:等待 58.9 秒
429 (openrouter_free_tier_per_minute),第 2 次尝试:等待 60.1 秒
429 (openrouter_free_tier_per_minute),第 2 次尝试:等待 57.8 秒
60 个请求全部在 89 秒内完成
全部 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 运行了它:
constURL="https://openrouter.ai/api/v1/chat/completions";
constHEADERS={
Authorization:`Bearer ${process.env.OPENROUTER_API_KEY}`,
"Content-Type":"application/json",
};
constsleep=(ms:number)=>newPromise((resolve)=>setTimeout(resolve, ms));
constrandomBetween=(min:number,max:number)=>min+Math.random()*(max-min);
/** 429 之后等待的秒数:优先 Retry-After,其次 X-RateLimit-Reset,最后退避。 */
exportfunctionretryDelay(headers:Headers,attempt:number):number{
constretryAfter=headers.get("Retry-After");
if(retryAfter) {
constseconds=Number(retryAfter);
if(!Number.isNaN(seconds))returnseconds;
constwhen=Date.parse(retryAfter);// HTTP 日期
if(!Number.isNaN(when))returnMath.max(0, (when-Date.now())/1000);
}
constreset=headers.get("X-RateLimit-Reset");// Unix 时间,单位为毫秒
if(reset) {
// 在重置后把客户端分散到几秒内,避免它们同时重试。
returnMath.max(0, (Number(reset)-Date.now())/1000)+randomBetween(1,5);
}
returnMath.min(2**attempt,60)*randomBetween(0.5,1);// 带抖动的指数退避
}
exportasyncfunctionchat(body:object,maxAttempts=6):Promise<any> {
letres:Response|undefined;
for(letattempt=0; attempt<maxAttempts; attempt++) {
res=awaitfetch(URL, { method:"POST", headers:HEADERS, body:JSON.stringify(body) });
if(res.status!==402&&res.status!==429) {
if(!res.ok)thrownewError(`HTTP ${res.status}: ${awaitres.text()}`);
returnres.json();
}
consterror=(awaitres.json()).error;
constsource=error?.metadata?.limit_source;
// 来自在途预算的 402 会在 Retry-After 后清除;其他 402 需要额度。
if(res.status===402&&source!=="openrouter_in_flight_budget") {
thrownewError(`HTTP 402: ${JSON.stringify(error)}`);
}
if(source==="openrouter_free_tier_daily") {
thrownewError(`HTTP 429: ${JSON.stringify(error)}`);// 在 UTC 午夜重置,所以不要一直等到那时
}
if(attempt===maxAttempts-1)break;// 尝试次数已用尽,失败前不要再等待
constwait=retryDelay(res.headers, attempt);
console.log(`${res.status} (${source}), attempt ${attempt+1}: waiting ${wait.toFixed(1)}s`);
awaitsleep(wait*1000);
}
thrownewError(`Still rate limited after ${maxAttempts} attempts`);
}
// 同一个批处理任务:60 条提示词,每次并发 10 条。
asyncfunctionmain() {
constprompts=Array.from({ length:60}, (_,i)=>i);
constresults:any[]=[];
constworker=async()=>{
for(leti=prompts.shift(); i!==undefined; i=prompts.shift()) {
results[i]=awaitchat({
model:"poolside/laguna-s-2.1:free",
messages: [{ role:"user",content:`Reply with the number ${i}.`}],
max_tokens:16,
});
}
};
constt0=Date.now();
awaitPromise。all(Array.from({ length:10}, worker));
constcompleted=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),第 1 次尝试:等待 34.9s
429 (openrouter_free_tier_per_minute),第 1 次尝试:等待 34.6s
429 (openrouter_free_tier_per_minute),第 1 次尝试:等待 35.1s
429 (openrouter_free_tier_per_minute),第 1 次尝试:等待 33.5s
429 (openrouter_free_tier_per_minute),第 2 次尝试:等待 60.5s
429 (openrouter_free_tier_per_minute),第 2 次尝试:等待 58.6s
429 (openrouter_free_tier_per_minute),第 2 次尝试:等待 60.6s
60 of 60 completed in 106s
总耗时取决于批次开始时窗口所处的位置,以及模型提供方的繁忙程度。我们的两次 TypeScript 运行分别耗时 218 秒和 106 秒,两次 Python 运行分别耗时 92 秒和 89 秒。四次运行都完成了全部请求。
第 3 步:将溢出请求发送到备用模型
如果可以接受来自第二个模型的回答,你完全不必等待。提供方的速率限制针对的是某个提供方上的某个模型,因此换一个模型就会使用独立的容量。免费额度上限是例外,因为账户下所有 :free 模型共享这一上限。使用 models 参数时,我们会按顺序尝试每个模型,并在遇到任何错误(包括 429)时切换到下一个模型,这一切都发生在请求到达你的代码之前:
importos
importtime
fromcollectionsimportCounter
fromconcurrent.futuresimportThreadPoolExecutor
importrequests
URL="https://openrouter.ai/api/v1/chat/completions"
HEADERS={"Authorization":f"Bearer{os.environ['OPENROUTER_API_KEY']}"}
defjob(i):
r=requests.post(URL,headers=HEADERS,timeout=120,json={
# 先尝试免费模型;如果被限流,OpenRouter 会把请求发送到下一个模型。
"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()
returnr.status_code, data.get("model"), data.get("usage",{}).get("cost",0)
t0=time.time()
withThreadPoolExecutor(max_workers=10)aspool:# 同一个批处理任务:60 个提示词,每次 10 个
结果=list(pool.map(job,range(60)))
print(f"finished in{time.time()-t0:.0f}s")
print(Counter((status, model)forstatus, model, _inresults))
print(f"total cost: ${sum(costfor*_, costinresults):.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 s | $0 |
| 等待重置后重试 | 60 个中的 60 个 | 89 s | $0 |
| 模型回退 | 60 个中的 60 个 | 13 s | $0.000066 |
如果任务可以等待,就在重置时重试。如果不能等待,并且第二个模型的答案足够好,就添加一个回退。
第 4 步:让另一个提供方服务同一个模型
第 2 步和第 3 步处理了我们的免费模型配额问题。付费模型会遇到另一种限制:为其提供服务的提供方的容量。OpenRouter 上的大多数模型由多个提供方服务,每个提供方都有自己的速率限制。当某个提供方返回 429 时,我们会在错误到达你之前,向下一个提供方重试该请求。此功能默认开启。
我们向 google/gemma-4-31b-it 发送了 300 个请求,每次 50 个,并检查了每个请求的 GET /api/v1/generation 记录中的 provider_responses 列表,该列表显示了我们尝试过的每一个提供方:
300 个请求,每次 50 个,用时 11s
状态码:Counter({200: 300})
已读取的 generation 记录:300 个中的 300 个
服务提供方:{'CoreWeave': 177, 'DeepInfra': 118, 'Novita': 1, 'Venice': 1, 'ModelRun': 1, 'Parasail': 2}
需要多个提供方的请求:0
失败的尝试,按提供方和状态码:[]
总成本:$0.00130
分布在六个提供商上,没有任何请求需要重试。然后我们用 "provider": {"order": ["crusoe"]} 将同一个模型固定到 Crusoe,并发送了 150 个请求,每次 50 个。以下是一次开启回退的运行结果:
150 个请求,每次 50 个,用时 5 秒
状态码:Counter({200: 150})
读取的生成记录:150 / 150
由以下提供商处理:{'CoreWeave': 79, 'Crusoe': 71}
需要多个提供商的请求:79
失败的尝试,按提供商和状态码:[(('Crusoe', 429), 79)]
总成本:$0.00056
Crusoe 对 79 个请求进行了限流,CoreWeave 响应了全部 79 个,客户端看到 150 次成功。我们在关闭和开启 allow_fallbacks 的情况下重复了多次固定运行:
| allow_fallbacks | 运行次数 | 每次运行成功数 | 每次运行的 429 数 |
|---|---|---|---|
| false | 4 | 150, 147, 118, 136 | 0, 3, 32, 14 |
| true | 3 | 150, 150, 150 | 0 |
Crusoe 的容量每分钟都在变化,因此关闭回退时的失败数从 0 到 32 不等。开启回退时,Crusoe 在三次运行中分别对 21、41 和 79 个请求进行了限流,而客户端从未看到其中任何一个。任何缩小可服务请求提供商范围的做法,都会减少吸收突发流量的容量。如果你在 OpenRouter 上仍然看到提供商 429,请检查是否有 allow_fallbacks: false、过短的 only 列表或过紧的 max_price。要优先使用某个提供商又不失去故障转移,请设置 order 并将 allow_fallbacks 保留为默认值。
同样 300 个请求的突发流量发送到 anthropic/claude-haiku-5.5 和 openai/gpt-6-luna 时没有任何失败尝试。当闭源模型自身的 API 容量耗尽时,错误通常是 529 而不是 429,Claude 529 过载指南对此进行了说明。
如何完全避免触达速率限制?
上述修复方法处理的是 429 发生之后的情况。以下习惯可以降低其发生的可能性:
-
将并发数限制在限额允许的范围内。并发正是让我们第一次测试超限的原因。将请求节奏控制在略低于限额,比在 429 之后等待整个窗口结束所损失的时间更少。
-
将 max_tokens 设置得接近你预期的答案长度。OpenAI 会将 max_tokens 计入你的每分钟 token 限额,因此过于宽裕的取值会比答案本身更快地耗尽限额。
-
在大任务之前检查你的密钥。GET /api/v1/key 会报告每日免费模型计数器。在这些运行之后,我们读到的是 {'used': 585, 'limit': 1000, 'remaining': 415}。在你购买至少 10 个 credits 之前,每日限制是 50 次免费模型请求,之后是 1,000 次。
-
在生产环境中使用付费变体。每分钟 20 次的限制仅适用于 :free模型。
常见问题
如何提高我的 OpenAI API 速率限制?
当你的组织累计购买的 credit 达到每个使用层级阈值时,OpenAI 会自动提高你的限制。你可以在 OpenAI 仪表盘的 Settings > Organization > Limits 下查看当前限制。通过 OpenRouter,提供商路由会将模型的流量分散到所有提供该模型的提供商,因此单个提供商的限制不会限制你。
为什么 Claude 会说“rate limit reached”?
在 Claude API 上,这意味着你的组织用完了该模型每分钟的请求数或 token 数,retry-after 头会说明需要等待多少秒。如果 429 来自你的月度支出上限,则没有 retry-after 头,在访问恢复之前重试也无济于事。在订阅版 Claude Code 中,该消息通常表示套餐的使用限制。参见触发了 Claude Code 使用限制?。
更多 API key 能绕过速率限制吗?
不能。在 OpenRouter 上,额外的 API key 或账户不会改变你的速率限制,因为我们在全局范围内管理容量。提供商限制按模型适用,这就是回退模型有效的原因,但你账户上的每个 :free 模型共享一个免费层级上限。使用BYOK时,使用你自己提供商 key 的请求会计入你在该提供商处的限制。
Retry-After 的单位是秒还是毫秒?
秒,或一个 HTTP 日期。我们的 X-RateLimit-Reset 不同。它是 Unix 时间戳,单位为毫秒。第 2 步中的 retry_delay 函数处理所有三种形式。
后续步骤
-
限制:免费模型上限、X-RateLimit-* 头以及如何处理 402 和 429 错误。
-
提供商路由:order、allow_fallbacks、only 和排序。
-
模型回退:models 参数以及回退请求如何计费。
-
免费 LLM API 对比:各提供商的免费层级限制。