Error reference

OpenAI 429 Rate Limit Errors: What They Mean and How to Fix Them

Updated July 27, 2026

In short
An OpenAI 429 is two different problems sharing one status code. rate_limit_exceeded means you went over requests-per-minute or tokens-per-minute for your usage tier. It is transient and exponential backoff fixes it. insufficient_quota means your billing balance or spend limit is exhausted. It is not retryable, and a retry loop only makes things worse. Always read error.code in the response body, not just the HTTP status.
start here
client = OpenAI(max_retries=5)  # SDK retries 429s with exponential backoff
Diagnosis

Causes at a glance

CauseHow to recognize itFix
Requests per minute (RPM) exceedederror.code is rate_limit_exceeded and x-ratelimit-remaining-requests is 0Back off until x-ratelimit-reset-requests elapses; queue or spread the calls
Tokens per minute (TPM) exceededrate_limit_exceeded mentioning "tokens per min"; x-ratelimit-remaining-tokens is 0Shrink prompts and right-size max_completion_tokens. It is reserved against TPM
No credit balance or spend cap reachederror.code is insufficient_quota; message points at plan and billing detailsAdd credit or raise the usage limit in Billing. Retrying never clears this
Burst concurrency from parallel workers429s spike on deploy or on a fan-out, then disappear on their ownAdd a shared token-bucket limiter and jitter so retries do not resynchronise
Usage tier too low for the workloadSteady 429s even with correct backoff; the remaining-* headers sit near zeroMove up a usage tier, split traffic across models, or shift work to the Batch API

A 429 is two errors, and only one of them is retryable

OpenAI returns HTTP 429 for both throttling and billing. rate_limit_exceeded is throttling: your organization went over the requests-per-minute or tokens-per-minute allowance for that model on your usage tier, and the allowance refills continuously. insufficient_quota is billing: the prepaid balance is empty or a hard monthly spend limit was hit.

The distinction decides your entire handling strategy. A generic "retry all 429s" wrapper turns a billing problem into an infinite loop that burns requests, fills logs, and hides the real cause for hours. Branch on error.code before you retry anything.

# HTTP 429 — the status is the same, the body is not
{
  "error": {
    "message": "You exceeded your current quota, please check your plan and billing details.",
    "type": "insufficient_quota",
    "code": "insufficient_quota"
  }
}
# insufficient_quota = billing. No amount of retrying will ever succeed.
Step 1

Step 1: read the x-ratelimit-* response headers

Every response carries the current state of both limits. Whichever remaining value is zero tells you which limit you actually hit, and the matching reset value tells you how long until it refills. These headers are on successful responses too, so you can watch headroom shrink before anything fails.

In the Python SDK the parsed response object does not expose headers: use with_raw_response to get them.

x-ratelimit-limit-requests: 5000
x-ratelimit-remaining-requests: 0
x-ratelimit-reset-requests: 12ms
x-ratelimit-limit-tokens: 450000
x-ratelimit-remaining-tokens: 128
x-ratelimit-reset-tokens: 6.5s

# Python: read them from any call
raw = client.chat.completions.with_raw_response.create(model="gpt-4o-mini", messages=msgs)
print(raw.headers.get("x-ratelimit-remaining-tokens"))
completion = raw.parse()
Step 2

Step 2: retry with exponential backoff and jitter

The official SDKs already retry 429 and 5xx responses with exponential backoff and honour retry-after; max_retries defaults to 2. Raising it is often the entire fix. Write your own loop only when you need behaviour the SDK does not give you: such as refusing to retry insufficient_quota.

Jitter is not optional. Without it, every worker that got throttled in the same second retries in the same second, and the 429 wave repeats at each backoff step instead of draining.

import random, time
from openai import OpenAI, RateLimitError

client = OpenAI(max_retries=0)  # take over retries ourselves

def call(**kwargs):
    for attempt in range(6):
        try:
            return client.chat.completions.create(**kwargs)
        except RateLimitError as e:
            if getattr(e, "code", None) == "insufficient_quota":
                raise  # billing, not throttling — do not retry
            hinted = e.response.headers.get("retry-after")
            delay = float(hinted) if hinted else min(2 ** attempt, 60)
            time.sleep(delay + random.uniform(0, 1))  # jitter
    raise RuntimeError("still rate limited after 6 attempts")
Step 3

Step 3: cut the tokens you spend per minute

TPM accounting reserves the completion budget up front: the request is charged against your token limit using the prompt plus the maximum tokens it could generate. An oversized max_completion_tokens therefore consumes TPM even when the model answers in two sentences. Right-sizing it is usually the single largest TPM win available.

Streaming does not help here. Streaming changes how the response is delivered, not how many tokens are consumed or billed: the same prompt and completion count identically against TPM whether you stream or not. It is a latency and timeout tool, not a rate-limit tool.

# max_completion_tokens is reserved against TPM before generation starts.
# A 200-token answer with a 4096 ceiling still charges ~4096 against TPM.
resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    max_completion_tokens=512,   # right-size it to the real answer length
)
print(resp.usage.prompt_tokens, resp.usage.completion_tokens)
Step 4

Step 4: move tolerant workloads to the Batch API

Evaluations, backfills, bulk classification, and offline enrichment do not need a synchronous response. The Batch API takes a JSONL file of requests, runs them within a 24-hour window, costs 50% less per token, and draws on a separate queue allowance instead of your synchronous RPM/TPM budget.

Shifting that traffic off the interactive path frees the whole limit for the requests a user is waiting on, which is often cheaper and faster to ship than a tier upgrade.

# batch.jsonl — one JSON object per line
# {"custom_id":"row-1","method":"POST","url":"/v1/chat/completions","body":{"model":"gpt-4o-mini","messages":[...]}}

f = client.files.create(file=open("batch.jsonl", "rb"), purpose="batch")
batch = client.batches.create(
    input_file_id=f.id,
    endpoint="/v1/chat/completions",
    completion_window="24h",
)
print(batch.id, batch.status)

Usage tiers: when more limit is genuinely the answer

Rate limits scale by usage tier, and tiers advance on cumulative spend plus time since your first payment. Limits are enforced per model, so a tier that comfortably serves a small model can still throttle a frontier one, and the two do not share a pool.

Ask for more limit only after backoff, right-sized max_completion_tokens, and batching are all in place. If the remaining-token header still sits near zero across the whole minute at steady state, the workload genuinely exceeds the tier and no client-side change will fix it.

Beyond the one-off fix

A 429 tells you the quota is gone; it never tells you who spent it. When a dozen coding agents, CI jobs, and terminals share one API key, the throttled user is rarely the one who caused it. Nexus captures every token_usage event through runtime hooks and attributes it per user, repo, and terminal, so a rate-limit incident resolves to a specific agent loop on a specific branch, and when a fallback provider takes over mid-incident, that switch is recorded rather than invisible.

Explore Swfte Nexus

Common questions

What is the difference between rate_limit_exceeded and insufficient_quota?
Both return HTTP 429, but `rate_limit_exceeded` is throttling that clears within seconds, while `insufficient_quota` means your billing balance or spend limit is exhausted. Only the first is retryable: retrying an `insufficient_quota` error will never succeed and just burns requests.
How long should I wait before retrying an OpenAI 429?
Use the `retry-after` header when it is present, otherwise exponential backoff starting around one second and capped at roughly a minute, plus random jitter. The `x-ratelimit-reset-requests` and `x-ratelimit-reset-tokens` headers tell you exactly when each limit refills.
Does streaming reduce OpenAI token usage or rate limits?
No. Streaming only changes how the response is delivered: the same prompt and completion tokens are consumed and billed either way, and they count identically against your tokens-per-minute limit. Use streaming for perceived latency and to avoid long request timeouts, not to dodge a 429.
Why do I get 429 errors when my usage dashboard shows low volume?
Rate limits are per minute, not per day, so a short burst from parallel workers can exceed RPM or TPM while the daily total looks tiny. An oversized max_completion_tokens also reserves that budget against TPM up front, so a few concurrent requests can exhaust the limit before the model generates anything.
Should I request a rate limit increase or fix my client?
Fix the client first: add backoff with jitter, right-size the completion ceiling, and move latency-tolerant work to the Batch API. Request more limit only when the remaining-token header stays near zero across whole minutes at steady state, which means the workload genuinely exceeds your usage tier.