How Errors Arrive
Codex is a GraphQL API, so most errors come back in theerrors array of an otherwise normal response — HTTP 200 with an errors key, not an HTTP error status. Authentication, payment (402), malformed queries (400), rate limiting and infrastructure failures (502, 503, 504, and an HTML 403 from the edge firewall) fail at the HTTP layer instead; see the table below.
Which Errors to Retry
There is one field to watch: if an error carriesextensions.retryAfterSeconds, wait that many seconds before retrying that request. On rate-limited responses the same value is also sent as the standard Retry-After header, so most HTTP client libraries will honor it without any code from you.
Rate limited
ATOO_MANY_REQUESTS error means you exceeded your plan’s per-second rate limit. When we can tell you exactly when the limit lifts, we do, via retryAfterSeconds and the Retry-After header.
A 429 without retryAfterSeconds means your request budget is momentarily empty rather than your account being throttled — capacity typically returns within a second. Back off on your own schedule and retry; don’t retry immediately in a tight loop.
See Rate Limits & Connection Limits for the limits themselves.
Over capacity
AnOVER_CAPACITY error means the data your query needs is temporarily under more load than it can serve, and we’re scaling up. It is not caused by anything wrong with your request — the same query will succeed once you retry.
These arrive as HTTP 200 with the error in the body. Retry after retryAfterSeconds (currently 30). Retrying sooner is likely to be throttled and slows the recovery for everyone.
Request errors
NOT_FOUND, INVALID_ARGUMENT, BAD_USER_INPUT, NOT_AUTHORIZED and GRAPHQL_VALIDATION_FAILED mean the request can’t succeed as written, so retrying won’t help. Branch on extensions.code rather than the message text: messages are written for people and can change.
One exception has no code. An input value written inline in the query that doesn’t match its type, such as a missing required field, returns HTTP 200 with only a message. Pass inputs as variables and the same mistake returns 400 BAD_USER_INPUT.
Forbidden (403)
AFORBIDDEN error means the API key was recognized but the request is not allowed right now. The message says why:
Over WebSocket, the same conditions close the connection with code
4403 and the message as the close reason. An account that becomes paused while a subscription is running is disconnected within about a minute with the reason Forbidden.
On compute-unit plans (see Compute Units), two more 403 messages can appear:
With auto-scale off, using the whole allowance pauses every key on the account and returns
Your account has exceeded its usage limit for this billing period, please upgrade your plan until the period rolls over. Turn auto-scale on to keep requests working past the allowance.
Everything else
Unexpected errors return a generic message with an error code:Retry Strategy
Fixed retry intervals are the most common cause of prolonged rate limiting. If your interval is shorter than the penalty window, every retry lands inside it and extends the problem.
- Honor
retryAfterSecondswhenever it’s present. It’s computed from the actual condition — it isn’t a guess, and it overrides whatever interval you’d otherwise use. - Otherwise use exponential backoff with jitter. Jitter matters if you run multiple workers: without it they synchronize and retry in a thundering herd.
- Cap your retries. Three to five attempts is plenty; past that the condition needs attention rather than another request.
- Don’t retry non-retriable errors. Auth failures and malformed queries will fail identically every time.