HTTP 429 is surfaced as "exceeded retry limit" without retrying; expose retry_429 or clarify error

Open 💬 1 comment Opened Jun 28, 2026 by nyl199310

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:

  1. The user-facing message says exceeded retry limit, but in this path the client may not have retried the HTTP 429 at all.
  2. request_max_retries is user-configurable, but users cannot make HTTP 429 retryable because retry_429 is not exposed in config.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:

  1. Configure a provider normally, with default request_max_retries.
  2. Have the Responses endpoint return an HTTP 429 before a successful SSE stream is established.
  3. Observe that provider retry config has retry_429: false.
  4. The request is mapped to CodexErr::RetryLimit and displayed as exceeded retry limit, last status: 429 Too Many Requests.
  5. CodexErr::RetryLimit is 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 as rate limited: HTTP 429 Too Many Requests and include any useful reset/retry-after information if available.
  • If HTTP 429 is intended to be retryable, set retry_429: true for 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> to ModelProviderInfo.
  • Add it to core/config.schema.json.
  • In to_api_provider(), use something like retry_429: self.retry_429.unwrap_or(false) or a provider-specific default.
  • Consider honoring Retry-After headers for transport-level 429, not only delay text parsed from SSE response.failed messages.
  • Consider renaming or splitting RetryLimitReachedError so 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.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗