Fix OpenAI RateLimitError (429): The Complete Guide
Quick answer
OpenAI RateLimitError is an HTTP 429 with two different causes. If error.code is insufficient_quota, it is a billing problem — add a payment method or wait for your quota to reset; retries never help. If error.code is rate_limit_exceeded, you are sending requests or tokens too fast, so add exponential backoff with jitter, honour the Retry-After header, and cap concurrency. Always check error.code first to know which fix applies.
You call the OpenAI API, everything works in testing, and then in production you see this:
openai.RateLimitError: Error code: 429 - {
'error': {
'message': 'Rate limit reached for gpt-4o in organization org-xxxx on tokens per min (TPM): Limit 30000, Used 29500, Requested 800.',
'type': 'tokens',
'code': 'rate_limit_exceeded'
}
}Or, more confusingly, you see a 429 on your very first request, before you could possibly be "too fast":
openai.RateLimitError: Error code: 429 - {
'error': {
'message': 'You exceeded your current quota, please check your plan and billing details.',
'type': 'insufficient_quota',
'code': 'insufficient_quota'
}
}Both raise the same Python class — openai.RateLimitError — and both are HTTP
429. But they are two different problems with opposite fixes. The single
biggest mistake I see engineers make is wrapping the first case's solution
(retries and backoff) around the second case (billing), then wondering why the
retries loop forever and burn latency without ever succeeding.
This guide separates the two cleanly, then goes deep on the throughput fix that most articles skim: reading the rate-limit headers, capping concurrency, and building a client-side budget so you throttle before the API does.
Quick Answer
openai.RateLimitError is an HTTP 429 with two causes. Read error.code first.
If it is insufficient_quota, it is a billing problem — add a payment method
or wait for the quota to reset; retrying never helps. If it is
rate_limit_exceeded, you are sending requests or tokens too fast, so add
exponential backoff with jitter, honour the Retry-After header, and cap
concurrency.
TL;DR
- Two causes, one exception.
insufficient_quota= billing.rate_limit_exceeded= speed. SameRateLimitErrorclass. - Check
error.codebefore doing anything. It decides which fix applies. insufficient_quota: add a card, check hard limits, wait for reset. Retries are useless here.rate_limit_exceeded: exponential backoff + jitter, honourRetry-After, cap concurrency, shrinkmax_tokens.- Limits are per-model and enforced on RPM (requests/min) and TPM (tokens/min) at once.
- The SDK already retries 429 twice — raise
max_retries, don't reinvent it, but still guard billing errors yourself.
If you only remember one thing: retrying an insufficient_quota error will
never succeed. It is not a speed problem. No amount of backoff adds credit to
your account. Detect it and fail fast.
Diagnose First: Which 429 Do You Have?
Every fix below depends on this branch. Here is the fastest way to read the branch in code — catch the error, print the code, and route accordingly:
from openai import OpenAI, RateLimitError
client = OpenAI()
try:
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello"}],
)
except RateLimitError as e:
# e.code is the sub-type; e.status_code is 429 for all of these
print("status:", e.status_code) # 429
print("code:", e.code) # 'insufficient_quota' or 'rate_limit_exceeded'
print("message:", e.message)e.status_code→429in every case.e.code→ the deciding field:insufficient_quotaorrate_limit_exceeded.e.message→ the human-readable reason, often naming the exact limit hit.
On very old code you may still see import openai with
openai.error.RateLimitError. That is the pre-1.0 SDK (before November 2023).
The error submodule was removed in v1.0. If you get
AttributeError: module 'openai' has no attribute 'error', you are mixing v0
patterns with a v1 install — upgrade the code, not the pin.
Common Causes
| # | Cause | error.code | Category |
|---|---|---|---|
| 1 | No payment method / expired free grant | insufficient_quota | Billing |
| 2 | Monthly usage cap (hard limit) reached | insufficient_quota | Billing |
| 3 | Too many requests per minute (RPM) | rate_limit_exceeded | Throughput |
| 4 | Too many tokens per minute (TPM) | rate_limit_exceeded | Throughput |
| 5 | One huge prompt exceeding the whole TPM ceiling | rate_limit_exceeded | Throughput |
| 6 | Bursty concurrency (many parallel calls at once) | rate_limit_exceeded | Throughput |
| 7 | Oversized max_tokens reserved against TPM | rate_limit_exceeded | Throughput |
| 8 | Shared org key used by several services at once | rate_limit_exceeded | Throughput |
Causes 1–2 are money and settings. Causes 3–8 are pacing. They do not overlap,
which is exactly why the error.code check matters.
Symptoms
- A 429 on the first call of a fresh key → almost always
insufficient_quota. - 429s that appear only under load or in bursts →
rate_limit_exceeded. - The message names a limit like
tokens per min (TPM): Limit 30000→ TPM throughput. - The message says
You exceeded your current quota→ billing. - Intermittent 429s that vanish when you add a
sleep()between calls → RPM pacing. - 429s that scale with prompt size, not request count → TPM, often via a large
max_tokens.
Root Cause
Why insufficient_quota happens
OpenAI meters spend against your account balance and any hard usage limit you set
in the dashboard. When either is exhausted, the platform rejects requests with a
429 whose code is insufficient_quota. It reuses the 429 status — the same one
used for speed limits — which is the source of nearly all the confusion. The
request never reached the model; it was refused at the billing gate.
Why rate_limit_exceeded happens
Rate limits are per-model and measured on several axes at once, most importantly:
- RPM — requests per minute
- TPM — tokens per minute (prompt tokens plus your reserved
max_tokens)
You trip the limit the instant either axis is exceeded inside a rolling 60-second window. Two non-obvious consequences:
- A single request can trip TPM by itself. If your prompt plus
max_tokensis larger than the model's TPM ceiling, no amount of pacing helps — you must shrink the request or move to a higher tier. max_tokensis reserved up front. The API counts the maximum you might generate against TPM before generation starts, even if the model returns far fewer tokens. An unnecessarily largemax_tokenssilently eats your budget.
Rough token math: for the throughput axis, budget roughly
prompt_tokens + max_tokens per request against TPM. If you send 50 requests
that each reserve 4,000 tokens in the same minute, that is ~200,000 tokens
requested — which will 429 on any tier whose TPM is below that, regardless of
how few tokens the model actually returns.
Step-by-step Solution
Path A — Fix insufficient_quota (billing)
This path has no code. Retrying is the wrong instinct here.
- Open the OpenAI dashboard → Settings → Billing. Confirm a valid payment method is attached and there is available credit.
- Check your usage limits. Settings → Limits shows your monthly hard limit. If usage has reached it, raise the limit or wait for the reset.
- Confirm you are billing the right org/project. If your key belongs to a project with no budget, calls fail even though another project has credit. Regenerate the key under the funded project, or set the project in the client.
- Fail fast in code so a billing outage does not masquerade as a slow service:
from openai import OpenAI, RateLimitError
client = OpenAI()
def chat(messages):
try:
return client.chat.completions.create(model="gpt-4o", messages=messages)
except RateLimitError as e:
if e.code == "insufficient_quota":
# Do NOT retry — surface a clear, actionable error instead.
raise RuntimeError(
"OpenAI quota exhausted. Check billing/usage limits."
) from e
raise # let a real rate_limit_exceeded fall through to the retry layerThe point of line-level detail here: e.code == "insufficient_quota" is the
guard that stops your retry logic from looping on an unfixable error. Everything
else re-raises so your throughput handling (Path B) can take over.
Path B — Fix rate_limit_exceeded (throughput)
Step 1: Use the SDK's built-in retries (the 80% fix)
Before writing any retry code, know that the official SDK already retries 429s twice with exponential backoff. Most "my app 429s occasionally" problems are solved by simply raising the ceiling:
from openai import OpenAI
# Retries connection errors and 429/5xx up to 5 times, with backoff + jitter.
client = OpenAI(max_retries=5)
# Or per-call, so you can be more aggressive only where it matters:
resp = client.with_options(max_retries=8).chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Summarise this ticket..."}],
)Node/TypeScript is identical in spirit:
import OpenAI from "openai";
const client = new OpenAI({ maxRetries: 5 });
try {
const resp = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "Hello" }],
});
} catch (err) {
if (err instanceof OpenAI.RateLimitError) {
// err.status === 429; err.code tells you the sub-type
console.error(err.status, err.code, err.message);
}
}Step 2: Custom exponential backoff with jitter
When you need control the SDK doesn't give you — custom logging, metrics, or a
non-OpenAI client — use tenacity. Jitter is not optional: without it, many
workers that failed at the same instant all retry at the same instant and
stampede the API again.
import random
from openai import OpenAI, RateLimitError
from tenacity import (
retry, stop_after_attempt, wait_exponential_jitter, retry_if_exception_type
)
client = OpenAI(max_retries=0) # turn off SDK retries so tenacity owns it
@retry(
retry=retry_if_exception_type(RateLimitError),
wait=wait_exponential_jitter(initial=1, max=60, jitter=5),
stop=stop_after_attempt(6),
reraise=True,
)
def chat(messages):
return client.chat.completions.create(model="gpt-4o", messages=messages)wait_exponential_jitter(initial=1, max=60)→ waits ~1s, 2s, 4s, 8s… capped at 60s, plus random jitter so retries spread out.stop_after_attempt(6)→ give up after 6 tries instead of hammering forever.max_retries=0on the client → prevents the SDK and tenacity both retrying (which would multiply your effective attempts).
Don't double-retry. If you add your own retry layer, set the SDK's
max_retries=0. Otherwise 6 tenacity attempts × 3 SDK attempts = up to 18 real
requests per logical call — which makes rate limiting worse, not better.
Step 3: Honour Retry-After and read the rate-limit headers
The best fix is to not guess the wait at all. On a 429, OpenAI often sends a
Retry-After header telling you exactly how long to wait. And on every
response it sends your live budget. Read it with the raw response:
from openai import OpenAI
client = OpenAI()
raw = client.chat.completions.with_raw_response.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hi"}],
)
h = raw.headers
print("remaining requests:", h.get("x-ratelimit-remaining-requests"))
print("remaining tokens: ", h.get("x-ratelimit-remaining-tokens"))
print("resets in (tokens):", h.get("x-ratelimit-reset-tokens")) # e.g. '6ms', '1.2s'
resp = raw.parse() # get the normal typed response backThe full set of headers:
| Header | Meaning |
|---|---|
x-ratelimit-limit-requests | Your RPM ceiling for this model |
x-ratelimit-limit-tokens | Your TPM ceiling for this model |
x-ratelimit-remaining-requests | Requests left in the current window |
x-ratelimit-remaining-tokens | Tokens left in the current window |
x-ratelimit-reset-requests | Time until the request window resets |
x-ratelimit-reset-tokens | Time until the token window resets |
retry-after | (On 429) seconds to wait before retrying |
Reading remaining-tokens lets you throttle proactively — pause before you hit
zero instead of reacting to a 429 after the damage is done.
Step 4: Cap concurrency
Bursty parallelism is the most common production trigger. Fan out 200 tasks with
asyncio.gather and you send 200 requests in one instant — an easy RPM/TPM
overrun. A semaphore fixes it in three lines:
import asyncio
from openai import AsyncOpenAI
client = AsyncOpenAI(max_retries=5)
sem = asyncio.Semaphore(10) # at most 10 in-flight requests
async def chat(messages):
async with sem: # blocks the 11th caller until a slot frees
return await client.chat.completions.create(
model="gpt-4o", messages=messages
)
async def main(all_messages):
return await asyncio.gather(*(chat(m) for m in all_messages))Tune the semaphore to your tier. A limit of 10 in-flight requests turns a 1,000-item burst into a steady stream the API can absorb.
Step 5: Shrink the request
If a single call trips TPM, pacing won't save you — the request itself is too big. Options, cheapest first:
- Lower
max_tokensto the real ceiling you need. This directly reduces the reserved TPM. - Trim the prompt — drop redundant context, summarise history, use retrieval instead of stuffing documents.
- Route to a smaller model (for example
gpt-4o-mini) which typically has a much higher TPM ceiling at the same tier.
Verification Steps
Confirm the fix actually holds before you call it done:
- Reproduce the original failure deliberately — e.g. fire 100 concurrent
requests — and confirm the code no longer raises an unhandled
RateLimitError. - Log
e.codeon every caught 429. After the fix you should see zeroinsufficient_quota(billing is healthy) and only rare, recoveredrate_limit_exceeded. - Watch the headers.
x-ratelimit-remaining-tokensshould dip toward zero under load and never sit pinned at zero for long. - Check retry counts. If your metrics show requests retrying 5+ times
routinely, your steady-state rate is too high — lower concurrency, don't just
raise
max_retries. - Confirm end-to-end latency didn't balloon. Backoff that always fires means you are over your limit continuously and should throttle upstream.
Green state: occasional single-retry 429s that recover transparently, a
remaining-tokens header that breathes up and down, and no insufficient_quota
in your logs. That is a healthy client running near — but under — its limit.
Alternative Solutions
| Approach | Best for | Trade-off |
|---|---|---|
SDK max_retries | Most apps; quickest fix | Coarse; no custom logic |
tenacity / backoff | Custom retry policy, metrics | You own the correctness |
| Semaphore concurrency cap | Bulk fan-out jobs | Needs tuning per tier |
| Client-side token budget | High-throughput services | More code to maintain |
| Batch API | Non-urgent bulk (e.g. nightly) | Up to 24h turnaround |
| Provider fallback | Hard availability needs | Extra vendor + prompt parity |
| Request a higher tier | Genuinely need more headroom | Tied to cumulative spend |
The Batch API is the underused answer
Most guides stop at retries. If your workload is bulk and not latency-sensitive — generating embeddings for a corpus, classifying a backlog, offline evals — the Batch API sidesteps the whole problem. It runs against a separate rate limit pool, costs ~50% less, and completes within a 24-hour window. Moving batch work off your synchronous limits frees that entire budget for real-time traffic.
Multi-provider fallback
For availability-critical paths, catch a persistent 429 and fail over to a second provider or a smaller model. Keep the prompt and the parsing identical so the fallback is a drop-in:
from openai import OpenAI, RateLimitError
primary = OpenAI(max_retries=3)
def resilient_chat(messages):
try:
return primary.chat.completions.create(model="gpt-4o", messages=messages)
except RateLimitError as e:
if e.code == "insufficient_quota":
raise # billing — failing over won't help the root cause
# throughput exhausted: degrade to a smaller, higher-TPM model
return primary.chat.completions.create(model="gpt-4o-mini", messages=messages)Platform & Framework Notes
Azure OpenAI
Azure meters differently. Each deployment gets its own TPM quota that you
assign in the portal, so a 429 usually means that specific deployment is
undersized — raise its TPM allocation or spread load across regions. Azure is
also stricter about honouring Retry-After; always read and respect it. The
exception class differs too: with the SDK you catch openai.RateLimitError, but
the message and remedy point at deployment quota, not account billing.
LangChain
LangChain wraps the OpenAI client, so a raw RateLimitError still surfaces. It
also has its own retry knob:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="gpt-4o",
max_retries=6, # LangChain-level retries with backoff
request_timeout=60,
)Set retries either on the LangChain object or the underlying client, not both, to avoid the double-retry multiplication described earlier. For debugging a LangChain 429, see our guide on resolving LangChain ValidationError.
Serverless & CI/CD
In serverless (Lambda, Cloud Functions) every concurrent invocation is a separate process, so an in-process semaphore does not bound total concurrency across invocations. Use provisioned-concurrency limits, a queue (SQS, Pub/Sub), or a shared token bucket in Redis to cap the fleet, not just one worker. In CI, parallel test shards hammering a shared org key is a classic self-inflicted 429 — give CI its own key/project with a modest limit.
Prevention
A checklist for keeping 429s out of production:
- Set
max_retriesexplicitly (don't rely on the default of 2). - Guard
insufficient_quotaso it fails fast and pages billing, not on-call. - Cap concurrency with a semaphore sized to your tier.
- Keep
max_tokensas small as the task allows. - Read
x-ratelimit-remaining-tokensand throttle before hitting zero. - Move bulk/offline work to the Batch API.
- Alert on billing usage at 80% of your hard limit — before it 429s.
- Give CI and each service its own project/key so limits are isolated.
- Add jitter to every retry to avoid synchronised stampedes.
- Load-test at expected peak, not average, before shipping.
Best practices
- Throttle at the source. A client-side token budget that paces requests is worth more than any retry policy — retries treat the symptom, throttling treats the cause.
- Separate billing alarms from rate alarms. They page different people:
insufficient_quotais finance/ops;rate_limit_exceededis engineering. - Right-size the model.
gpt-4o-miniat a high TPM often beatsgpt-4ofighting its limit — measure quality on your task before assuming you need the larger model.
Troubleshooting Matrix
| Symptom | Likely cause | Fix |
|---|---|---|
| 429 on first-ever call | insufficient_quota | Add payment method / credit |
| 429 only under burst load | RPM overrun | Cap concurrency (semaphore) |
| 429 scales with prompt size | TPM overrun | Lower max_tokens, trim prompt |
| One big prompt always 429s | Request exceeds TPM ceiling | Smaller model or higher tier |
| Retries loop, never succeed | Retrying insufficient_quota | Guard and fail fast |
| 429s worse after adding retries | Double-retry multiplication | Set client max_retries=0 |
AttributeError: openai.error | Pre-1.0 code on v1 SDK | Upgrade code to v1 patterns |
| Azure 429 with healthy billing | Deployment TPM too low | Raise deployment quota |
Related Guides
- Fix "Token indices sequence length is longer than the specified maximum"
- Resolve LangChain ValidationError
- Resolve ChromaDB collection does not exist
- Resolve Hugging Face 401 Unauthorized
- Handling API errors and timeouts in production LLM apps
External References
- OpenAI — Rate limits guide
- OpenAI — Error codes reference
- OpenAI Python SDK (GitHub)
- OpenAI — Batch API
- tenacity — retrying library docs
FAQs
Is insufficient_quota the same as running out of free credits?
Often, yes. An expired free grant with no payment method attached has zero usable
quota, which returns insufficient_quota. But a paying account that hit its
monthly hard limit gets the exact same code. Both are billing/settings, not
speed.
Should I catch RateLimitError or a broader class?
Catch RateLimitError specifically so you can branch on e.code. For a
belt-and-braces production wrapper, also handle APITimeoutError and
APIConnectionError (transient network) and APIStatusError (other HTTP
errors), but keep the 429 logic separate — its remedy is unique.
How long should my backoff cap be?
Cap at 30–60 seconds and limit total attempts to ~6. Longer waits mean you are
structurally over your limit and should throttle upstream instead. If a
Retry-After header is present, prefer it over your computed delay.
Do rate limits reset exactly every 60 seconds? They are enforced over a rolling window, not a fixed clock minute, and can be sub-divided (a 60 RPM limit is effectively ~1 request/second). This is why short high-volume bursts trip limits even when your per-minute average looks fine.
Can I have separate limits for embeddings and chat? Yes. Limits are per-model and per-endpoint. Heavy embedding jobs and chat traffic draw from different pools, so an embeddings backfill won't consume your chat TPM — plan them independently.
Key takeaways
- •A 429 RateLimitError is not one bug — inspect error.code first: insufficient_quota is billing, rate_limit_exceeded is throughput.
- •insufficient_quota will never be fixed by retrying or backoff; it means no credit or a hard usage limit was hit.
- •For rate_limit_exceeded, use exponential backoff with jitter and honour the Retry-After and x-ratelimit-reset headers.
- •Rate limits are per-model and enforced on both requests per minute (RPM) and tokens per minute (TPM) — a single large prompt can trip TPM alone.
- •max_tokens is reserved against your TPM budget before generation, so an oversized max_tokens can cause 429s on its own.
- •In production, cap concurrency with a semaphore, add a client-side token budget, and route bulk work to the Batch API.
Frequently asked questions
What does OpenAI RateLimitError mean?
It is an HTTP 429 response. It means either your account has no available quota (error code insufficient_quota, a billing issue) or you sent requests or tokens faster than your per-minute limit allows (error code rate_limit_exceeded, a throughput issue). The two need opposite fixes, so always read error.code before reacting.
Why do I get RateLimitError on my very first request?
Almost always insufficient_quota, not speed. A new account with no payment method, or one whose free grant has expired, has zero usable quota, so even the first call returns 429. Add a payment method in Billing and confirm you have credit — backoff and retries cannot fix a quota of zero.
Does the OpenAI SDK retry 429 errors automatically?
Yes. The official Python and Node SDKs retry 429 (and connection errors) twice by default using exponential backoff. Raise the ceiling with max_retries (Python) or maxRetries (Node). It will still retry insufficient_quota pointlessly, so guard billing errors yourself instead of relying on retries.
How do I avoid hitting OpenAI rate limits in production?
Cap concurrent requests with a semaphore, track a client-side token budget so you throttle before the API does, keep max_tokens tight, batch multiple inputs per call where the endpoint allows it, and send non-urgent bulk jobs through the Batch API, which has its own separate limits.
What is the difference between RateLimitError and insufficient_quota?
They are not opposites — insufficient_quota is one kind of RateLimitError. The SDK raises RateLimitError for any 429. The error.code field tells you the sub-type: insufficient_quota (billing) or rate_limit_exceeded (throughput). Same Python exception class, completely different remedy.
How do I read my remaining OpenAI rate limit from the response?
Use the raw response so you can see headers: client.chat.completions.with_raw_response.create(...). The response exposes x-ratelimit-remaining-requests, x-ratelimit-remaining-tokens, and x-ratelimit-reset-tokens. Read them to throttle before you are blocked instead of reacting to 429s after the fact.