HTTP 429 is surfaced as "exceeded retry limit" without retrying; expose retry_429 or clarify error
What version of Codex CLI is running?
Source review against main at bdd282f3bbd55df3a869a5438519cd948c134d4d (2026-06-27).
What subscription do you have?
N/A - source-review / reliability issue.
Which model were you using?
N/A.
What platform is your computer?
N/A.
What issue are you seeing?
There are many user reports of errors like:
exceeded retry limit, last status: 429 Too Many Requests, request id: ...
After tracing the current source, this message appears misleading for at least one common path: a direct HTTP 429 from the Responses HTTP/WebSocket wrapper does not appear to be retried by the local request retry policy, because retry_429 is hard-coded to false for providers.
Relevant code:
// codex-rs/model-provider-info/src/lib.rs
let retry = ApiRetryConfig {
max_attempts: self.request_max_retries(),
base_delay: Duration::from_millis(200),
retry_429: false,
retry_5xx: true,
retry_transport: true,
};
The retry machinery does support 429 in principle:
// codex-rs/codex-client/src/retry.rs
TransportError::Http { status, .. } => {
(self.retry_429 && status.as_u16() == 429)
|| (self.retry_5xx && status.is_server_error())
}
But since providers set retry_429: false, a direct HTTP 429 falls through and is mapped here:
// codex-rs/codex-api/src/api_bridge.rs
} else if status == http::StatusCode::TOO_MANY_REQUESTS {
...
CodexErr::RetryLimit(RetryLimitReachedError {
status,
request_id: extract_request_tracking_id(headers.as_ref()),
})
}
The displayed text then comes from:
// codex-rs/protocol/src/error.rs
"exceeded retry limit, last status: {}{}"
And CodexErr::RetryLimit(_) is explicitly non-retryable in CodexErr::is_retryable(), so the turn-level stream retry loop will not retry it either.
This creates two problems:
- The user-facing message says
exceeded retry limit, but in this path the client may not have retried the HTTP 429 at all. request_max_retriesis user-configurable, but users cannot make HTTP 429 retryable becauseretry_429is not exposed inconfig.toml/ModelProviderInfo.
This is distinct from SSE response.failed rate-limit events. Those can become ApiError::Retryable and use the stream retry path. The confusing behavior here is for direct HTTP 429 responses from the transport/wrapper layer.
What steps can reproduce the bug?
Source-level reproduction / audit:
- Configure a provider normally, with default
request_max_retries. - Have the Responses endpoint return an HTTP 429 before a successful SSE stream is established.
- Observe that provider retry config has
retry_429: false. - The request is mapped to
CodexErr::RetryLimitand displayed asexceeded retry limit, last status: 429 Too Many Requests. CodexErr::RetryLimitis non-retryable, so the turn fails instead of entering the stream retry/backoff path.
What is the expected behavior?
At minimum, the behavior and message should be aligned:
- If HTTP 429 is not retried by design, do not call it
exceeded retry limit; display a clearer message such asrate limited: HTTP 429 Too Many Requestsand include any useful reset/retry-after information if available. - If HTTP 429 is intended to be retryable, set
retry_429: truefor appropriate providers or expose it as a provider config option.
A possible config shape:
[model_providers.openai]
request_max_retries = 8
retry_429 = true
Potential implementation points:
- Add
retry_429: Option<bool>toModelProviderInfo. - Add it to
core/config.schema.json. - In
to_api_provider(), use something likeretry_429: self.retry_429.unwrap_or(false)or a provider-specific default. - Consider honoring
Retry-Afterheaders for transport-level 429, not only delay text parsed from SSEresponse.failedmessages. - Consider renaming or splitting
RetryLimitReachedErrorso direct 429s are not mislabeled as retry exhaustion.
Additional information
Related user reports include many issues with this exact text, for example:
- #11434
- #15468
- #18540
- #22122
- #23304
- #24657
- #26034
- #26655
In #26034, a maintainer noted an active incident and said it was not client-side. This issue is not disputing that some 429s are service-side incidents. It is specifically about the local client behavior and wording when a transport-level HTTP 429 is received.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗