Skip to content

exceeded retry limit, last status: 429 Too Many Requests

This line is a report, not a failure. It is written by the client after its retry policy ran out, and it summarizes several requests rather than describing one. That already rules out most of the classes people check: the request was well-formed (a malformed one returns 400 and is not retried), authentication worked (401 is not retried either), and the platform was not down — model overload is 503 with server_is_overloaded, a different status that a handler keyed on rate limiting will miss entirely. Every attempt reached OpenAI and every attempt was refused. The question is which refusal, and whether the loop ever had a chance.

Data as of 2026-09. Vendor limits and defaults change; check the official docs for current values before acting on any number below.

The report kept the least informative field

last status: 429 is the HTTP status of the final attempt. Three things it does not tell you, each of which matters more:

Were all the attempts the same? A run of identical 429s is a pacing problem. A run that started with 500s or 503s and ended on a 429 is a capacity event that pushed your traffic into a burst on recovery — same ending, different cause, different fix. Only your client’s logs have the earlier statuses.

What was error.code? “429 Too Many Requests” is the generic HTTP reason phrase, and it is identical across at least five distinct conditions. error.code is what separates them, and it is the field the summary drops. Several of those conditions could never have succeeded no matter how many attempts the policy allowed — the documentation is blunt about it: “Retrying billing, spend, or quota errors won’t restore API access. Update the relevant credits or limits before sending another request.”

How many requests actually went out? Probably more than the retry count suggests — see below.

Also worth knowing before you go looking for a vendor page about it: the attempt count and backoff schedule are your client’s, not OpenAI’s. An SDK, a CLI, or your own wrapper chose them. If you want to change that number, it is configured wherever the client is configured, and the provider has no say in it.

Is retrying useful?

Yes for an ordinary rate limit — but the retry that works is a slower one, not a longer one.

Honor Retry-After when it is present and valid; otherwise use exponential backoff with jitter, and cap both the attempt count and the total retry time. Do not invent a fixed wait: the documented guidance is “short exponential backoff” and no base delay or cap is published for you to copy, and the same docs note that handling of Retry-After — especially long delays — varies by SDK version and configuration.

That variance is itself a cause of this message. An SDK may return the original HTTP error rather than retry when the server-requested delay exceeds the maximum it supports. So “exceeded retry limit” can mean the server asked for a wait your client was unwilling to take, not that the server refused you indefinitely. Check the Retry-After value on the final response before concluding anything: a large value there changes the diagnosis from “I am being throttled” to “my client will not wait that long.”

And the hard bound on the verdict: if error.code names credits, spend, or a usage limit, retrying was never going to work, and the whole run was a loop reproducing a certainty.

Retrying spends the thing you are waiting for

The instinct after reading “exceeded retry limit” is to raise the retry limit. It is the wrong direction, and the reason is specific rather than philosophical: failed requests still count against your per-minute limit. The documentation states this outright.

So a retry loop against a rate limit is not neutral while it waits — it consumes the allowance it is waiting to get back. Each refused attempt pushes recovery further out. Doubling the attempts doubles the consumption and delays the moment the window clears. This is the mechanism behind the experience of a client that “keeps failing harder the longer it runs”.

There is a second multiplier that catches people with their own wrappers. The official SDKs already retry eligible failures — connection errors, 408, 409, 429 and 5xx — twice by default. A loop of your own around that does not add attempts, it multiplies them: five outer attempts over an SDK doing two inner retries is fifteen requests, not five, and the count in this message is the outer one. The docs say what to do about it: “If you manage retries in your application, disable SDK retries or account for them.” Until you do, your real request volume is not the number you think you configured, and it is the real volume the limit counts.

Which 429 you were looping against

Each of these has an observation that confirms it, and you need the response body or headers from one of the failed attempts — which is the argument for logging error.code and x-request-id on every failure before you need them.

  • error.code is absent and Retry-After is present — an ordinary rate limit on requests or tokens. Retrying was correct; the policy was just too aggressive to let the window clear.
  • error.code: slow_down with error.type: rate_limit_error — a ramp-rate limit. The docs are explicit that this “can occur even when your traffic is within its requests-per-minute and tokens-per-minute limits. It reflects how quickly traffic increased, not whether you exhausted those limits.” The confirming observation is headroom: x-ratelimit-remaining-tokens still healthy while you are being refused.
  • error.code: credit_balance_exhausted — prepaid credits are gone. Every attempt in your run failed for this and every future one will.
  • organization_spend_limit_exceeded or project_spend_limit_exceeded — a cap you configured. Nothing client-side clears it.
  • organization_usage_limit_exceeded — the usage limit OpenAI assigns your tier, which is a different number from the spend limits you set yourself. Needs a limit increase, not a retry.
  • A 503 earlier in the run — model overload rather than your usage. Note that SDKs raise InternalServerError for 503 and RateLimitError for 429, so a handler catching only the rate-limit class treats overload as an unhandled exception.

Two header details to read correctly while you are in there: the x-ratelimit-reset-* values are duration strings like 6m0s and 1s, not timestamps, and the project-scoped token headers appear only when a project-scoped limit applies — their absence is not evidence of headroom.

Fix by what you found

  • Ordinary rate limit — reduce concurrency before you touch anything else. Per-minute limits are about arrival rate, and halving in-flight requests clears them faster than any prompt change. Then add jitter, so a batch of clients that failed together does not retry together.
  • slow_down — introduce a ramp instead of going idle-to-full in one step. The documented rule of thumb is that once traffic reaches 1 million input tokens per minute, you should increase it by no more than 50% every 15 minutes — with the docs’ own hedge attached, that “the exact point at which the ramp-rate limit applies can vary by model and traffic conditions.” Treat it as a shape to copy, not a threshold to sit under.
  • Any billing, spend or quota code — stop retrying and fix the account side. Raising the retry limit here converts a clear failure into a slow one.
  • Nested retries — turn one of the two layers off. Keep the SDK’s and drop yours, unless you need per-request policy the SDK cannot express.
  • A Retry-After longer than your client tolerates — either let the client wait, or queue the work rather than looping on it. Batch endpoints exist for exactly this and are counted against separate queue limits rather than your per-minute pool.

Confirming the fix

“It worked when I ran it again” is not evidence: limits refill, and a single success is indistinguishable from having waited while you edited the config.

Re-run the same workload at the same volume for several minutes and count two things — total retries attempted, and x-ratelimit-remaining-requests and -tokens at their lowest point. A fixed workload is one where the retry count falls to near zero and the remaining counters never bottom out, not one where the final request happened to land. If you changed the ramp, the falsifiable version is that the first minute of the run no longer produces refusals, since the ramp-rate case fails at the beginning of a burst rather than at its peak.

For the account-side cases the confirmation is simpler and binary: the first request that previously returned a 429 with a billing or quota error.code returns a normal response. Nothing but the account change can produce that transition, which is exactly what makes it a real test.

If the body you captured says the quota is exhausted rather than the rate too high — You exceeded your current quota, please check your plan and billing details — then the retry run never had a chance and the fix is entirely on the billing side. The same condition surfaced through the Python SDK appears as a RateLimitError carrying insufficient_quota, which is the case most likely to be misread as throttling because of the exception class it arrives in. And if you are sizing concurrency rather than diagnosing a single run, the API rate limit calculator works in the dimension that actually refused you — arrival rate against your tier — which is the number this message compressed away.