A 429 from OpenAI means you crossed one of five ceilings — and the first job is working out which one, because the fixes point in opposite directions. This page covers what each limit measures, how to read the answer straight off the response headers, the retry logic that actually stops the errors, and what to do when correct backoff isn't enough.
Verified August 5, 2026 against OpenAI's rate limits documentation.
Five limits, any one of which can fire
| Metric | Measures | Typically bites when |
|---|---|---|
| RPM | Requests per minute | Many small calls — classification, embeddings, agent loops |
| TPM | Tokens per minute | Few large calls — RAG with big retrieved context, long documents |
| RPD | Requests per day | Free and low tiers; a batch job that finishes the daily quota |
| TPD | Tokens per day | Same, measured in tokens |
| IPM | Images per minute | Image generation workloads |
Whichever is exhausted first triggers the error, so “we're nowhere near the token limit” is not a reason to rule out a rate limit — you may be nowhere near TPM and sitting exactly on RPM. Limits apply per organization and per model, not per key: minting extra keys does not mint extra quota.
What a 429 looks like
HTTP/1.1 429 Too Many Requests
retry-after: 12
x-ratelimit-limit-requests: 500
x-ratelimit-remaining-requests: 0
x-ratelimit-reset-requests: 12s
x-ratelimit-limit-tokens: 200000
x-ratelimit-remaining-tokens: 143820
x-ratelimit-reset-tokens: 17s
{
"error": {
"message": "Rate limit reached for gpt-5.4 in organization org-... on requests per min (RPM).",
"type": "requests",
"code": "rate_limit_exceeded"
}
}Everything you need is in that response. The body names the dimension (“requests per min (RPM)”), and the headers give you the exact ceiling, what's left, and when it recovers.
| Header | Meaning |
|---|---|
retry-after | Minimum seconds to wait before retrying |
x-ratelimit-limit-requests | Maximum requests permitted before exhausting the limit |
x-ratelimit-remaining-requests | Requests left before exhausting it |
x-ratelimit-limit-tokens | Maximum tokens permitted |
x-ratelimit-remaining-tokens | Tokens left |
x-ratelimit-reset-requests / -reset-tokens | Time until each counter resets — they reset independently |
Usage tiers
Your ceilings are set by your usage tier, which OpenAI promotes automatically as cumulative spend accumulates:
| Tier | Qualification | Monthly usage limit |
|---|---|---|
| Free | User in an allowed geography | $100 / month |
| Tier 1 | $5 paid | $100 / month |
| Tier 2 | $50 paid | $500 / month |
| Tier 3 | $100 paid | $1,000 / month |
| Tier 4 | $250 paid | $5,000 / month |
| Tier 5 | $1,000 paid | $200,000 / month |
Deliberately not reproduced here: the per-model RPM and TPM numbers. They differ per model, change as models ship, and can be adjusted per account — so any table of them published on a third-party page is a guess with a date on it. The two authoritative sources for your own account are the limits page in the OpenAI dashboard and the x-ratelimit-* headers on every response you already make. Read the headers.
The fix: honour Retry-After, then jitter
OpenAI's documented recommendation is exponential backoff with jitter, following Retry-After when the response carries one. Both halves matter. Without the header you may retry too early; without the jitter, every client that failed in the same instant retries in the same instant and fails together — a thundering herd that turns one bad second into a bad minute.
import random, time
import openai
client = openai.OpenAI()
def call_with_backoff(fn, *, max_attempts=6, base=0.5, cap=30.0):
"""Retry 429s: honour Retry-After when present, jittered backoff otherwise."""
for attempt in range(max_attempts):
try:
return fn()
except openai.RateLimitError as err:
if attempt == max_attempts - 1:
raise
# The server's own answer beats any formula you invent.
retry_after = (err.response.headers or {}).get("retry-after")
if retry_after:
delay = float(retry_after)
else:
# Full jitter: sleep a random point in [0, 2^n * base], capped.
# Without the randomness every client that failed at the same
# instant retries at the same instant and fails again together.
delay = random.uniform(0, min(cap, base * 2**attempt))
time.sleep(delay)
resp = call_with_backoff(lambda: client.responses.create(
model="gpt-5.4",
input="Summarise this changelog in three bullets.",
))The same shape in TypeScript:
import OpenAI from "openai";
const client = new OpenAI();
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export async function callWithBackoff<T>(
fn: () => Promise<T>,
{ maxAttempts = 6, baseMs = 500, capMs = 30_000 } = {},
): Promise<T> {
for (let attempt = 0; ; attempt++) {
try {
return await fn();
} catch (err) {
const status = (err as { status?: number }).status;
if (status !== 429 || attempt === maxAttempts - 1) throw err;
const retryAfter = (err as { headers?: Headers }).headers?.get("retry-after");
const delay = retryAfter
? Number(retryAfter) * 1000
: Math.random() * Math.min(capMs, baseMs * 2 ** attempt);
await sleep(delay);
}
}
}
const resp = await callWithBackoff(() =>
client.responses.create({ model: "gpt-5.4", input: "Hello" }),
);The official SDKs already retry 429s for you, so most applications need this only when they wrap calls in their own HTTP client, or when they want different behaviour — a longer ceiling for background work, or an immediate failure for a user-facing request where a 12-second wait is worse than an error.
Monitor before it breaks
The counters are on every response, not just failures. Logging the remaining values turns rate limiting from an incident into a gauge — you can see the headroom shrinking days before a launch exhausts it.
# Log the remaining counters on every response, not just on failures.
# By the time you see a 429 the useful signal is already an hour old.
resp = client.responses.with_raw_response.create(model="gpt-5.4", input="…")
h = resp.headers
log.info(
"openai_quota model=%s req_left=%s tok_left=%s reset_req=%s reset_tok=%s",
"gpt-5.4",
h.get("x-ratelimit-remaining-requests"),
h.get("x-ratelimit-remaining-tokens"),
h.get("x-ratelimit-reset-requests"),
h.get("x-ratelimit-reset-tokens"),
)
parsed = resp.parse() # the normal response objectTwo habits worth having alongside it: alert on x-ratelimit-remaining-tokens dropping below some fraction of the limit rather than on 429 count, and add jitter to schedules, not just retries. A cron that fires everything at :00 manufactures its own burst.
When backoff isn't the answer
Correct backoff fixes bursts. It does nothing for sustained demand above your ceiling — there, retries just move the failure later. The structural fixes, roughly in order of effort:
- Cap output. Reasoning tokens bill and count as output, so an unbounded generation is the fastest way to eat TPM.
- Trim retrieved context. On a TPM ceiling, halving the retrieved chunks doubles your throughput for free.
- Right-size the model. A classification step does not need a frontier model, and small models have their own separate budget.
- Separate the workloads. Batch jobs and latency-sensitive traffic competing for one org-level ceiling is the most common self-inflicted version of this problem.
- Raise the tier. Tiers move with cumulative spend, so this is often already happening.
Spreading load across model families
The last item on that list is where a gateway earns its place. Kunavo exposes an OpenAI-compatible API — same SDK, same call shape, one key — across several model families, so moving a workload off a saturated ceiling is a model-string change rather than a second integration:
from openai import OpenAI
client = OpenAI(
api_key="sk-kn-...",
base_url="https://api.kunavo.com/v1",
)
# Same SDK, same call shape — the model string chooses the family.
client.chat.completions.create(
model="gpt-5-4", # or claude-sonnet-4-6, gemini-2-5-flash, …
messages=[{"role": "user", "content": "Hello"}],
)Concretely: the batch summarisation job that was competing with your production traffic can run on gemini-2-5-flash at $0.09 / $0.75 per 1M or gpt-5-4-mini at $0.225 / $1.35, while the latency-sensitive path stays on gpt-5-4 ($1.00 / $6.00) or claude-sonnet-4-6 ($1.20 / $6.00). Different family, different queue.
Be clear about what this does and doesn't do. It removes the single-account-single-model bottleneck and it gives you somewhere to fail over to. It does not manufacture capacity: if your total volume genuinely exceeds what any one tier allows, the answer is still to raise the tier or do less work. Rates for every model are on the pricing page, and the equivalent playbook for Anthropic's limits is in Claude API 429 rate_limit_error.
FAQ
Why am I rate limited on a brand-new account?
Free and Tier 1 accounts carry daily ceilings (RPD and TPD) that higher tiers don't, so a modest test script can exhaust a day's allowance in an afternoon. Tier 1 unlocks at $5 of cumulative payment.
Does a 429 cost me anything?
No — a rejected request isn't processed and isn't billed. What it costs is latency, and whatever your retry logic does with that latency.
Will more API keys give me more throughput?
No. Limits are per organization and per model. Extra keys are useful for attribution and revocation, not for capacity.
Should I catch 429 or let the SDK handle it?
Let the SDK handle the common case, and catch it yourself where the default is wrong for you: a user-facing request that should fail fast, or a background job that can afford a much longer ceiling than the default.
What about 429s that are really quota exhaustion?
An exhausted monthly usage limit also surfaces as a 429, and no amount of backoff clears it — the message body distinguishes the two. If the counters in the headers are healthy but you're still refused, check billing before touching your retry code.