Skip to content

openai.RateLimitError: Error code: 429 insufficient_quota

You are reading a traceback, so start with what the exception object proves. A 429 came back, which means your credential was accepted (an invalid one returns 401), your region is allowed (403), and your request was well-formed (400). It also means the model had capacity — overload is a 503 and raises a different class entirely. The word insufficient_quota narrows it further, to the account’s budget rather than to the pace of your traffic — which matters because the two have opposite fixes and this exception class only names one of them.

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

The class name is the least informative field on the object

RateLimitError is not a diagnosis. The SDK maps HTTP status to exception class before anything looks at the body — 429 becomes RateLimitError the same way 401 becomes AuthenticationError and 400 becomes BadRequestError. Every 429 OpenAI can send arrives under this one name: prepaid credits exhausted, a spend cap you configured, the monthly usage limit assigned to your tier, an ordinary per-minute limit, and a ramp-rate refusal. Five conditions, four of which are not about request rate at all, sharing a class called RateLimitError.

That naming is why the reflex handler is wrong here:

except RateLimitError:
    time.sleep(backoff)
    continue

It reads as obviously correct and it is the single most expensive thing you can write for this error. It turns a condition that requires a human decision — add credits, raise a cap, request a limit increase — into an infinite loop that looks like progress. And it is not free: failed requests still count against your per-minute limit, so a tight loop on a budget refusal manufactures a genuine rate-limit problem on top of the billing one.

What insufficient_quota settles, and what it does not

insufficient_quota is documented as the broad billing type, not as one of the specific billing codes. OpenAI’s own guidance for billing errors is to inspect error.code, precisely because the type stays general while the codes — credits exhausted, an organization spend limit, a project spend limit, the organization’s assigned usage limit — are what distinguish the fixes.

So this string settles the category and stops one level short of the answer:

  • Settled: this is the billing family. Lowering concurrency, adding jitter, and pacing your requests are the wrong tools. They address a limit you are not hitting.
  • Not settled: which budget. Whose limit it is decides whether the fix is a payment, a settings change you can make yourself, or a request to OpenAI that you cannot make in code at all.

Be aware of how you came to be holding this string. Wrappers, agent frameworks and job runners flatten the exception for their logs, and which field they pick varies. If your line came from a framework rather than from an uncaught traceback, the token after 429 may be the type, the code, or the first thing the wrapper found. Catch the exception yourself once and read code off it directly rather than trusting the rendering.

Is retrying useful?

Yes — exactly once, and only if your client has not already spent that retry for you. Check that first.

The official SDKs retry 429 responses automatically, twice by default, with a short exponential backoff. That means by the time this exception surfaced in your except block, an identical request had already been sent and refused more times than you know about. The question “would retrying help” has been answered empirically before you thought to ask it, and the answer cost you requests against a limit that counts failures. If max_retries is at its default, skip the manual retry entirely — it has been done.

The single retry is worth making only when your client has retries disabled, and its purpose is to falsify the category rather than to fix anything. A time-varying condition can clear as a window moves; a budget condition returns the identical refusal instantly, and the docs are explicit that retrying billing, spend or quota errors will not restore access — you have to change the credits or the limits first. Honor Retry-After if it is present, and invent no delay of your own: no base value is published for you to copy, and the documented shape for hand-rolled clients is exponential backoff with jitter, capped on both attempts and total time. If you add your own loop, disable the SDK’s or account for it, or you silently multiply your request volume.

The handler hole that opened when overload moved

This is the part worth carrying away even after today’s problem is fixed.

Model overload used to be reachable through the throttling status. It is now documented as a 503 with server_is_overloaded, and in the Python, TypeScript and Ruby SDKs a 503 raises InternalServerError, not RateLimitError. Java splits the same way, with RateLimitException and InternalServerException. A handler written to catch only the throttling class therefore has a hole in it today that it did not have when it was written, and the symptom is an unhandled exception during a capacity event rather than during your own overuse.

The same applies in reverse: rapid traffic growth is refused with slow_down at 429, so it does land in your RateLimitError branch — and it is documented to happen even when you are inside your per-minute limits. A branch that assumes RateLimitError means “I sent too much” will mis-handle it.

What the except block should actually do

Branch on the code, not on the class, and keep the three outcomes separate:

  • A billing code — raise it to a human and stop. This is not a retryable condition and no client-side change reaches it. Log the code verbatim, because the four billing codes have four different owners.
  • A pacing condition — back off against Retry-After and reduce concurrency. The observation that confirms you are here is headroom, or the lack of it, in x-ratelimit-remaining-requests and x-ratelimit-remaining-tokens, both of which came back on the same response you are holding.
  • Anything else at 429 — treat as unknown, log, and do not loop.

Capture request_id off the caught exception in every branch. Failed requests still carry the x-request-id header, and a support conversation without it is a description of a feeling.

How to confirm it’s fixed

The confirmations differ by branch, and using the wrong one is how a fix gets declared and then reappears.

For a budget condition, one successful call is sufficient evidence. Nothing except an actual change to credits or limits can flip a deterministic refusal into a 200, so the transition itself is the proof — provided it is the same credential, the same project and the same model.

For your handler, the confirmation is separate and it is the one people skip: force the error path and watch what your code does. Set an org or project spend limit below your current usage deliberately, run the workload, and require that your handler stops rather than loops. If you cannot safely do that in production, do it in a scratch project. A retry loop that has never been observed hitting a non-retryable 429 is untested code on the exact path that pages you at 3am.

  • the raw quota body with a null code is this same condition seen from the other side — read it if you are holding a response body rather than an exception, because there the discriminating field is empty and the page is about where the answer lives instead.
  • exceeded retry limit, last status: 429 Too Many Requests is what this becomes once a client has looped on it and given up. If that is what you saw first, the attempts already spent are part of your problem.
  • openai.error.APIConnectionError is the contrasting case where no status arrived at all — useful when you are deciding which exception classes your handler needs to cover.
  • The API rate limit calculator answers the question this exception cannot: whether your intended request rate and token volume fit the tier you are on, which is the difference between a budget problem and a pacing one before you send anything.