OpenRouter 指南:如何修复 OpenAI 与 Claude API 的 429 限流错误
LLM API Rate Limits: How to Fix OpenAI and Claude 429 Errors
- 1OpenRouter 实测 OpenAI Python SDK 默认重试下,60 条提示词批次仍有 23 条因 429 失败
- 2按 Retry-After 或 X-RateLimit-Reset 等待重试后四次运行全部完成 60 条,Python 用时 89 到 92 秒
- 3用 models 参数把溢出请求转给备用付费模型,60 条 13 秒完成且无等待,总花费 $0.000066
- 4锁定单一提供商并关闭 failover 时 150 条请求单次最多失败 32 条,开启后每次全部成功
429 报错的有效解法取决于限流来自谁,按错误头里的 limit_source 分流处理,比默认 SDK 重试更能保证批任务完成。
- 影响:429 多在应用承受真实负载或批任务并发展开时出现,默认重试在窗口内撞墙既耗时又不保证成功;按重置时间等待能保全部完成但批次拉长到分钟级;备用模型与提供商 failover 用极小额费用换来无等待完成。
- 参考:可考虑在代码里按 limit_source 分流;能等待的批任务读 Retry-After 与 X-RateLimit-Reset 定时重试,不能等待的配置 models 备用模型;生产环境用付费模型并保留提供商 failover,排查 allow_fallbacks 设为 false、只列少数提供商或 max_price 过紧的配置。
- 依据:全部数字来自 OpenRouter 对自家 API 的实测并附代码与运行输出;平台路由与免费额度规则为当事方口径,暂无第三方复现。
A 429 Too Many Requests error means you sent more requests or tokens than your limit allows in the current time window. OpenAI, Anthropic and OpenRouter all return it, and it usually shows up the first time an app runs under real load or a batch job fans out across several workers.
Most people reach for a retry, and with default settings that often isn’t enough. When we ran a 60-prompt batch through the OpenAI Python SDK against a per-minute limit, 23 requests still failed after the SDK’s retries. The window reset 4 to 8 seconds after the SDK gave up on each one.
This guide starts from a real 429, reads its headers, and fixes it three ways: retry at the reset time the error gives you, send overflow to a fallback model, and let another provider serve the same model. We ran every example against the OpenRouter API, and the output under each code block is what we got.
Tl;dr
-
A 429 means you hit a limit in the current window. On OpenRouter, error.metadata.limit_source tells you whose limit it was.
-
With default settings, the OpenAI Python SDK retried each 429 twice and gave up within about 2 seconds. Against a per-minute limit, 23 of 60 requests in our batch still failed.
-
A retry that waits for Retry-After or X-RateLimit-Reset finished all 60 in every run, in 89 to 92 seconds in Python and 106 to 218 seconds in TypeScript.
-
A models fallback finished all 60 in 13 seconds, for $0.000066, with no waiting.
-
Pinned to one provider with failover off, up to 32 of 150 requests failed per run. With failover on, all 150 succeeded in every run.
What does a 429 rate limit error look like?
We sent 40 requests at once to poolside/laguna-s-2.1:free, a free model with a limit of 20 requests per minute. Thirty-eight succeeded, and two came back as 429 from the provider serving the model:
200: 38 429: 2 other: 0
429 limit_source: {'upstream_provider_shared_pool': 2}A 429 from our own free-tier limit carries a different limit_source. Here’s one from a separate run:
{
"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
}
}The headers carry the numbers you need. X-RateLimit-Limit is the cap, X-RateLimit-Remaining is what’s left, and X-RateLimit-Reset is the earliest time a retry can succeed, as a Unix timestamp in milliseconds. The body adds limit_source, which says whose limit you hit, and a remedy_hint written for people reading logs. Branch on limit_source in your code, not on the hint text.
OpenAI and Anthropic send the same information under different names. OpenAI’s x-ratelimit-reset-requests is a duration such as 1s, and both providers can send retry-after as a number of seconds.
Why do rate limits trigger at different request counts?
Thirty-eight of 40 requests got through a limit of 20. The limiter can accept requests that arrive at the same moment before the count catches up, and most limiters don’t reset cleanly at the top of each minute. Anthropic uses a token bucket that refills continuously. OpenAI can return a 429 with the code slow_down when your traffic ramps up too fast, even while you’re under your per-minute limits.
To see how our free-model limit counts, we sent 161 more requests in different patterns. Here are three of them:
| Test | Succeeded in that minute |
|---|---|
| Sequential, after two idle minutes | 30 |
| Sequential, the minute right after | 30 |
| 30 concurrent | 29 |
Two things came out of it. More than 20 requests succeeded in every minute we tested, so treat 20 as the point where 429s can start, not an exact count. When we hit it on poolside/laguna-s-2.1:free with 58 seconds left in the window, five requests to minimax/minimax-m3.1-flash:free went through right away, while Laguna stayed blocked. That doesn’t make the cap per model, though. OpenRouter applies one per-minute limiter to all free models on an account, so a request to a different free model can get the same 429.
Step 1: Read whose limit you hit
Waiting fixes a 429 only when the limit resets on a schedule, so check limit_source before you pick a fix:
| limit_source | Status | Whose limit | What fixes it |
|---|---|---|---|
| openrouter_free_tier_per_minute | 429 | Our free-model cap | Wait for X-RateLimit-Reset (Step 2), or fall back to a paid model (Step 3) |
| upstream_provider_shared_pool | 429 | The provider serving the model | Another provider (Step 4) or a fallback model (Step 3) |
| openrouter_in_flight_budget | 402 | Your in-flight spending budget | Wait for Retry-After and retry |
| openrouter_key_limit, openrouter_credits | 402 | Your key’s credit limit or your balance | Raise the key’s limit or add credits |

An upstream 429 means the provider serving the model is out of capacity. Providers don’t say when that capacity will free up, so these errors carried no reset header in our runs. The free variant of Gemma 4 31B returned this:
google/gemma-4-31b-it:free is temporarily rate-limited upstream. Please retry shortly, or add your own key to accumulate your rate limits.The same request with the paid variant as a fallback used a different provider’s capacity and succeeded at DeepInfra for $0.0000037.
Some errors that look like rate limits are about money. OpenAI and Anthropic both return a 429 when you reach a monthly spend cap, and retrying won’t fix it until the cap resets. On OpenRouter, credit problems come back as 402 instead.
Step 2: Retry at the reset time the error gives you
The OpenAI and Anthropic Python SDKs retry a 429 twice by default, with a short backoff that starts at 0.5 seconds and never waits more than 8. They follow a Retry-After header when the server sends one.
Our free-tier 429 sends X-RateLimit-Reset instead, which the SDKs don’t read, so both retries landed in the same full minute. Here’s a 60-prompt batch, 10 at a time, through the OpenAI SDK (openai 3.26.1) with default settings:
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 upThe SDK sent 112 HTTP requests for 60 prompts, and 23 still failed. Raising max_retries would eventually get through, but only by sending a request into a full window every 8 seconds, and OpenAI’s docs note that failed requests still count toward your per-minute limit. A better retry waits until the time the server gives you. To run all 60 prompts, use an account with at least 60 free-model requests left for the day, because accounts that haven’t bought 10 credits get 50 a day:
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 89sAll 60 requests finished, but the batch took 89 seconds instead of 12. Nothing fails with this approach, and every request that hits the limit waits out the window.
retry_delay checks Retry-After first because it’s the server’s direct instruction, while X-RateLimit-Reset only marks when the window rolls over. The random 1 to 5 seconds keeps the 10 workers from retrying at the same instant and filling the new window at once.
Even so, four requests in this run retried after the new window had already filled, so they waited a second time. If you wrap an SDK call in a loop like this, set max_retries=0 on the client, or each of your attempts turns into three requests.
The same retry in TypeScript
retry.ts follows the same order: Retry-After, then X-RateLimit-Reset, then backoff. It uses the built-in fetch, so it needs no packages. We ran it on Node 24 with 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 106sThe total time depends on where the window is when the batch starts and on how busy the model’s provider is. Our two TypeScript runs took 218 and 106 seconds, and our two Python runs took 92 and 89. All four finished every request.
Step 3: Send overflow to a fallback model
If an answer from a second model is acceptable, you don’t have to wait at all. A provider rate limit applies to one model at one provider, so a different model uses separate capacity. The free-tier cap is the exception, because every :free model on your account shares it. With the models parameter, we try each model in order and move to the next on any error, including a 429, before anything reaches your code:
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.000066The free model served 52 requests, and the paid model served the other 8. No request waited, and you pay only for the paid ones. The response’s model field records which model answered, and that’s the model you’re billed for.
Choose a fallback that has its own capacity. Every :free model on your account shares the free-model cap, so another free model isn’t a reliable fallback when that cap is the cause. Free models also run out of provider capacity often, so a paid fallback is the more reliable choice.
Here are the three runs of the same batch side by side:
| Run | Completed | Time | Cost |
|---|---|---|---|
| OpenAI SDK, default retries | 37 of 60 | 12 s | $0 |
| Retry that waits for the reset | 60 of 60 | 89 s | $0 |
| Models fallback | 60 of 60 | 13 s | $0.000066 |
If the job can wait, retry at the reset. If it can’t, and a second model’s answer is good enough, add a fallback.
Step 4: Let another provider serve the same model
Steps 2 and 3 handle our free-model cap. Paid models run into a different kind of limit: the capacity of whichever provider is serving them. Most models on OpenRouter are served by several providers, each with its own rate limits. When one provider returns a 429, we retry the request at the next provider before the error reaches you. This is on by default.
We sent 300 requests to google/gemma-4-31b-it, 50 at a time, and checked the provider_responses list in each request’s GET /api/v1/generation record, which shows every provider we tried:
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.00130Spread across six providers, no request needed a second try. Then we pinned the same model to Crusoe with “provider”: {“order”: [“crusoe”]} and sent 150 requests, 50 at a time. Here’s one run with fallbacks on:
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 rate-limited 79 requests, CoreWeave answered all 79, and the client saw 150 successes. We repeated the pinned run several times with allow_fallbacks off and on:
| allow_fallbacks | Runs | Succeeded per run | 429s per run |
|---|---|---|---|
| false | 4 | 150, 147, 118, 136 | 0, 3, 32, 14 |
| true | 3 | 150, 150, 150 | 0 |
Crusoe’s capacity changed from minute to minute, so the failures with fallbacks off ranged from none to 32. With fallbacks on, Crusoe rate limited 21, 41 and 79 requests across the three runs, and the client never saw one of them. Anything that narrows which providers can serve a request leaves less capacity to absorb a burst. If you still see provider 429s on OpenRouter, check for allow_fallbacks: false, a short only list or a tight max_price. To prefer one provider without losing failover, set order and leave allow_fallbacks at its default.
The same 300-request burst to anthropic/claude-haiku-5.5 and openai/gpt-6-luna had no failed attempts. When a closed model’s own API runs out of capacity, the error is usually a 529 rather than a 429, which the Claude 529 overloaded guide covers.
How do you avoid hitting rate limits at all?
The fixes above deal with a 429 after it happens. These habits make one less likely:
-
Cap concurrency at what the limit allows. Concurrency is what pushed our first test over the limit. Pacing requests just under the limit loses less time than waiting out a full window after a 429.
-
Set max_tokens close to the answer length you expect. OpenAI counts max_tokens toward your tokens-per-minute limit, so a generous value uses up the limit faster than the answers do.
-
Check your key before a large job. GET /api/v1/key reports the daily free-model counter. Ours read {‘used’: 585, ‘limit’: 1000, ‘remaining’: 415} after these runs. The daily limit is 50 free-model requests until you’ve bought at least 10 credits, and 1,000 after that.
-
Use paid variants in production. The 20-per-minute cap applies only to :free models.
Frequently asked questions
How do I increase my OpenAI API rate limit?
OpenAI raises your limits automatically as your organization’s total credit purchases pass each usage tier threshold. You can see your current limits under Settings > Organization > Limits in the OpenAI dashboard. Through OpenRouter, provider routing spreads a model’s traffic across every provider that serves it, so one provider’s limit doesn’t cap you.
Why does Claude say “rate limit reached”?
On the Claude API, it means your organization used up its requests or tokens per minute for that model, and the retry-after header says how many seconds to wait. If the 429 comes from your monthly spend cap, there’s no retry-after header, and retrying won’t help until access resumes. In Claude Code on a subscription, the message usually means the plan’s usage limit. See Hit your Claude Code usage limit?.
Do more API keys get around rate limits?
No. On OpenRouter, extra API keys or accounts don’t change your rate limits, because we govern capacity globally. Provider limits apply per model, which is why a fallback model works, but every :free model on your account shares one free-tier cap. With BYOK, requests that use your own provider key count against your limits at that provider.
Is Retry-After in seconds or milliseconds?
Seconds, or an HTTP date. Our X-RateLimit-Reset is different. It’s a Unix timestamp in milliseconds. The retry_delay function in Step 2 handles all three forms.
Next steps
-
Limits: free-model caps, the X-RateLimit-* headers and how to handle 402 and 429 errors.
-
Provider routing: order, allow_fallbacks, only and sorting.
-
Model fallbacks: the models parameter and how fallback requests are billed.
-
Free LLM APIs compared: free-tier limits across providers.
来源:OpenRouter:Blog(Announcements) · openrouter.ai