A 429 from the OpenAI API means the request was rejected before it reached a model. Check the response for a Retry-After header. If it is present, wait at least that many seconds before your next attempt, then retry with backoff and jitter. If it is absent, the 429 is more likely a spend limit, quota limit or organization-level cap that a retry will never clear, so stop retrying and surface the error instead of looping. The rest of this article covers how to read that header correctly, what to do when it is not there, and where the retry logic should live in a shipped PHP or JavaScript application.

What Triggers a 429 From OpenAI
OpenAI enforces limits per model and per organization, measured as requests per minute and tokens per minute. A short burst of calls can trip a limit even when your average traffic looks fine, because the check runs over a rolling window, not just a per-minute total. That is the case a Retry-After header is meant for: you asked for more than your current allowance permits, right now, and the answer is to wait.
Not every 429 is that case. The same status code also covers an exhausted prepaid balance, a spend limit set in the organization or project, and a usage limit. Those are billing and account states, not traffic states, and no amount of waiting fixes them. Your retry logic has to tell the two apart, because retrying a spend-limit error forever just burns your own compute cycles for nothing.
Reading the Retry-After Header Correctly
Retry-After is a standard HTTP header, not something OpenAI invented for its API. Per MDN’s Retry-After header reference (read 2026-09-18), it takes one of two forms: a plain non-negative integer meaning seconds to wait, or an HTTP-date after which to retry. The header is documented for 429 Too Many Requests and 503 Service Unavailable responses, and also appears on some redirects to mean the minimum wait before following them.
OpenAI’s rate limits guide (read 2026-09-18) says that when a request exceeds a temporary rate limit, the 429 response can include this header, and its own recommendation is to treat the value as a minimum: wait at least that long, then add a small random delay so that several clients backing off at once do not all retry in the same instant. Parse the header defensively. Confirm which of the two formats you received before you do arithmetic on it, and fall back to your own backoff if the value fails to parse as either.
When Retry-After Is Missing: Backoff With Jitter
A missing Retry-After header does not mean you should retry immediately, and it does not mean you should give up either. The standard fallback is exponential backoff with jitter: wait a short interval, and if the retry also fails with a rate-limit 429, double the wait and add a small random offset before trying again. The jitter matters for the same reason the header’s own guidance mentions it: a fleet of workers that all back off on the same fixed schedule will all retry in the same instant, recreating the burst that caused the 429 in the first place.
Cap both dimensions of this loop. Set a maximum number of attempts and a maximum total time spent retrying a single logical request, and give up cleanly once either limit is reached. An uncapped retry loop on a background worker is how a single rate-limited call turns into a queue backing up behind it while the process holds a connection or a database row open. Decide in advance what your application does when it gives up: fail the user-facing action with a clear message, or queue the work for a later attempt.
The x-ratelimit-* Headers Worth Reading Too
Beyond Retry-After, OpenAI’s rate limits guide describes a set of x-ratelimit-* response headers that report your standing against the request and token limits on every call, not only the ones that get rejected: remaining requests, remaining tokens, and the point at which each of those counters resets. Reading these on successful calls, not just failed ones, lets an application slow itself down before it ever hits a 429, by queuing or spacing out calls as the remaining count gets low, rather than reacting only after a rejection.
That is a meaningfully different strategy from reading Retry-After alone. Retry-After tells you what to do after a rejection has already happened; the x-ratelimit-* headers let you avoid the rejection. A production integration benefits from both: proactive throttling based on the remaining-count headers as the normal path, and Retry-After-driven backoff as the fallback for the bursts that get through anyway.
A Decision Order for Handling Every 429
Put the checks in this order so the code never retries something it shouldn’t:
| Signal in the response | What it means | What your code does |
|---|---|---|
| Error type says spend, quota or usage limit | Billing or account state | Do not retry. Alert a human, degrade the feature |
Retry-After header present | Temporary rate limit with a stated wait | Wait at least that long, add jitter, then retry |
Retry-After absent, error type is a rate limit | Temporary limit, no stated wait | Exponential backoff with jitter, bounded attempts |
| Same failure after the attempt cap | Retries exhausted | Stop, log the request ID and error type, fail cleanly |
Reading the error type before the Retry-After header matters more than it looks. A worker that checks for the header first and finds none can wrongly assume “must be a rate limit, back off and try again” on a spend-limit error that will never clear, looping until someone notices the bill or the alert queue. Check what kind of 429 it is first, and only then decide whether a wait time applies.
Where Retry Logic Belongs in a Shipped App
In a PHP application, especially a WordPress plugin, the temptation is to drop the OpenAI call and its retry loop straight into functions.php because that is the file that is already open. Resist it. A blocking retry loop with a multi-second sleep, run on every page load that happens to trigger it, is exactly the kind of change that turns a single slow dependency into a site-wide outage, which is the pattern covered in why editing functions.php keeps destroying your site. Give the integration its own class or file, called from a defined hook, so a bad retry loop fails one feature instead of the whole theme.
If a retry needs to happen without blocking the request that triggered it, that is a scheduling problem, not a loop problem. WordPress has a specific mechanism and a specific timing model for deferred work, and picking the wrong hook is a common way for a “retry in the background” plan to quietly run at the wrong point in the request lifecycle instead. Choosing the right hook for the job covers how to pick one deliberately rather than by habit.
Debugging a 429 That Keeps Coming Back
When a 429 shows up intermittently in production and you cannot reproduce it on demand, resist the urge to guess at a fix from memory of how the API “usually” behaves. Strip the failing call down to the smallest script that still reproduces it, isolated from the rest of the application, before you spend time on a fix or open a support ticket. Building a minimal reproduction before you ask anyone is the same discipline that applies to any intermittent bug, and it usually surfaces the actual trigger, a concurrency spike, a retry loop calling itself, faster than reading the stack trace alone does.
Once you know the pattern, log it. A small table recording each failed call’s timestamp, status code, error type and whatever Retry-After value it carried turns “it happens sometimes” into a chart you can act on. Building that logging query directly with string concatenation is how a debugging table becomes an attack surface; $wpdb->prepare and the queries that get sites hacked explains why every value in that insert, including anything taken from the API response, needs to go through a prepared statement.
FAQ
Does OpenAI’s Retry-After header use seconds or milliseconds?
Per the HTTP standard as described on MDN, Retry-After is either a count of seconds or an HTTP-date, not milliseconds. Check which form you received before doing arithmetic on it, and do not assume a unit.
Should I retry every 429 from OpenAI? No. OpenAI’s rate limits guide distinguishes a temporary rate limit, which is worth retrying with backoff, from spend, quota and usage limit errors, which a retry will not resolve. Read the error type in the response body, not just the 429 status.
What if OpenAI does not send a Retry-After header at all? Fall back to exponential backoff with jitter: wait, retry, and if it fails again, roughly double the wait and add a small random offset. Cap the number of attempts and the total time spent retrying so a stuck call cannot hold a worker indefinitely.
Can I rely on an official OpenAI SDK to handle this for me?
Official SDKs generally retry eligible rate-limit errors and honor Retry-After automatically. Check your SDK version’s retry settings before adding your own retry loop on top, since stacking two retry layers multiplies the actual number of attempts made against the API.
How many times should I retry before giving up? There is no single correct number; it depends on how long your user or process can wait. Pick a fixed maximum attempt count and a maximum total elapsed time up front, log every exhausted retry, and fail the request cleanly rather than looping silently.

