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. Same RateLimitError class.
  • Check error.code before 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, honour Retry-After, cap concurrency, shrink max_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?

RateLimitError decision tree: billing quota vs. throughput limitA 429 RateLimitError has two very different causes. Inspect the error code. insufficient_quota is a billing problem that retries never fix. rate_limit_exceeded is a throughput problem that backoff and concurrency control do fix.openai.RateLimitError(HTTP 429)What is error.code(or body.error.type)?insufficient_quotaBilling / quota problemNOT a speed problemAdd a payment methodCheck usage vs. hard limitWait for monthly resetRetries will NOT helprate_limit_exceededThroughput problemYou are going too fastBursty or steadytraffic?burstyAdd exponential backoff+ jitter, honourRetry-After headersustainedCap concurrency+ client-sidetoken/RPM budget
RateLimitError decision tree: billing quota vs. throughput limit

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_code429 in every case.
  • e.code → the deciding field: insufficient_quota or rate_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

#Causeerror.codeCategory
1No payment method / expired free grantinsufficient_quotaBilling
2Monthly usage cap (hard limit) reachedinsufficient_quotaBilling
3Too many requests per minute (RPM)rate_limit_exceededThroughput
4Too many tokens per minute (TPM)rate_limit_exceededThroughput
5One huge prompt exceeding the whole TPM ceilingrate_limit_exceededThroughput
6Bursty concurrency (many parallel calls at once)rate_limit_exceededThroughput
7Oversized max_tokens reserved against TPMrate_limit_exceededThroughput
8Shared org key used by several services at oncerate_limit_exceededThroughput

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:

  1. A single request can trip TPM by itself. If your prompt plus max_tokens is larger than the model's TPM ceiling, no amount of pacing helps — you must shrink the request or move to a higher tier.
  2. max_tokens is 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 large max_tokens silently 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.

  1. Open the OpenAI dashboard → Settings → Billing. Confirm a valid payment method is attached and there is available credit.
  2. Check your usage limits. Settings → Limits shows your monthly hard limit. If usage has reached it, raise the limit or wait for the reset.
  3. 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.
  4. 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 layer

The 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)

Request retry lifecycle: 429, header inspection, exponential backoffA request returns 429. The client reads the Retry-After and x-ratelimit-reset headers, waits base times two to the power of the attempt plus random jitter, then retries until a 200 or the max attempt ceiling.Send requestOpenAI API200 OKReturn response429Read Retry-After +x-ratelimit-resetWait base·2^n + jitterthen retry (n < max)retry
Request retry lifecycle: 429, header inspection, exponential backoff

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=0 on 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 back

The full set of headers:

HeaderMeaning
x-ratelimit-limit-requestsYour RPM ceiling for this model
x-ratelimit-limit-tokensYour TPM ceiling for this model
x-ratelimit-remaining-requestsRequests left in the current window
x-ratelimit-remaining-tokensTokens left in the current window
x-ratelimit-reset-requestsTime until the request window resets
x-ratelimit-reset-tokensTime 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_tokens to 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:

  1. Reproduce the original failure deliberately — e.g. fire 100 concurrent requests — and confirm the code no longer raises an unhandled RateLimitError.
  2. Log e.code on every caught 429. After the fix you should see zero insufficient_quota (billing is healthy) and only rare, recovered rate_limit_exceeded.
  3. Watch the headers. x-ratelimit-remaining-tokens should dip toward zero under load and never sit pinned at zero for long.
  4. 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.
  5. 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

ApproachBest forTrade-off
SDK max_retriesMost apps; quickest fixCoarse; no custom logic
tenacity / backoffCustom retry policy, metricsYou own the correctness
Semaphore concurrency capBulk fan-out jobsNeeds tuning per tier
Client-side token budgetHigh-throughput servicesMore code to maintain
Batch APINon-urgent bulk (e.g. nightly)Up to 24h turnaround
Provider fallbackHard availability needsExtra vendor + prompt parity
Request a higher tierGenuinely need more headroomTied 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_retries explicitly (don't rely on the default of 2).
  • Guard insufficient_quota so it fails fast and pages billing, not on-call.
  • Cap concurrency with a semaphore sized to your tier.
  • Keep max_tokens as small as the task allows.
  • Read x-ratelimit-remaining-tokens and 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

  1. 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.
  2. Separate billing alarms from rate alarms. They page different people: insufficient_quota is finance/ops; rate_limit_exceeded is engineering.
  3. Right-size the model. gpt-4o-mini at a high TPM often beats gpt-4o fighting its limit — measure quality on your task before assuming you need the larger model.

Troubleshooting Matrix

SymptomLikely causeFix
429 on first-ever callinsufficient_quotaAdd payment method / credit
429 only under burst loadRPM overrunCap concurrency (semaphore)
429 scales with prompt sizeTPM overrunLower max_tokens, trim prompt
One big prompt always 429sRequest exceeds TPM ceilingSmaller model or higher tier
Retries loop, never succeedRetrying insufficient_quotaGuard and fail fast
429s worse after adding retriesDouble-retry multiplicationSet client max_retries=0
AttributeError: openai.errorPre-1.0 code on v1 SDKUpgrade code to v1 patterns
Azure 429 with healthy billingDeployment TPM too lowRaise deployment quota

External References

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.


Related Posts