Back to guides
Troubleshooting·August 5, 2026·9 min read

OpenAI API rate limits — which limit you hit, how to read it, and the retry that fixes it

A 429 from OpenAI means one of five ceilings was crossed, and the fixes point in opposite directions depending on which. Here is how to read the answer straight off the response headers, and the retry logic that actually stops the errors.

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

MetricMeasuresTypically bites when
RPMRequests per minuteMany small calls — classification, embeddings, agent loops
TPMTokens per minuteFew large calls — RAG with big retrieved context, long documents
RPDRequests per dayFree and low tiers; a batch job that finishes the daily quota
TPDTokens per daySame, measured in tokens
IPMImages per minuteImage 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

response (HTTP 429)
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.

HeaderMeaning
retry-afterMinimum seconds to wait before retrying
x-ratelimit-limit-requestsMaximum requests permitted before exhausting the limit
x-ratelimit-remaining-requestsRequests left before exhausting it
x-ratelimit-limit-tokensMaximum tokens permitted
x-ratelimit-remaining-tokensTokens left
x-ratelimit-reset-requests / -reset-tokensTime 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:

TierQualificationMonthly usage limit
FreeUser 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.

backoff.py
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:

backoff.ts
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.

observe_quota.py
# 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 object

Two 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:

gateway.py
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.