Skip to content

[Bug]: A Codex turn retrying HTTP 429 sleeps as Working for hours instead of showing Limited #28936

Open

Description

@ntindle

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Summary

A provider can answer Codex with 429 and a Retry-After but without OpenAI's usage_limit_reached body. CLIProxyAPI does this once every account behind it is out of quota (model_cooldown). Codex 0.166 then retries and sleeps until Retry-After, with no cap. T3 shows the turn as Working with "Retrying provider (1/5)" for the whole sleep: hours, or days for a weekly window. It never shows Limited. The retry is recorded as a transport error, although T3 already treats the same 429 as a usage limit once retries are exhausted (#16288, #17501).

Steps to reproduce

  1. Point a Codex provider instance at a Responses endpoint that answers every POST /v1/responses with 429, Retry-After: 10800 and a body without error.type: "usage_limit_reached", for example {"error":{"code":"model_cooldown","message":"All credentials for model gpt-6-astra are cooling down"}}. A CLIProxyAPI instance whose Codex credentials are all out of quota answers this way.
  2. Start a Codex thread and send any message.
  3. Watch the thread.

Expected behavior

The turn shows as Limited, with Resume. The docs define Limited as "the provider stopped on a usage or rate limit", and T3 already shows it when the same 429 exhausts Codex's retries.

Actual behavior

  • The thread stays Working with "Retrying provider (1/5)".
  • The retry item has class transport_error, code responseStreamDisconnected, message exceeded retry limit, last status: 429 Too Many Requests.
  • Codex reports every retry it schedules as responseStreamDisconnected with httpStatusCode: 429 (notify_stream_error in codex-rs). It then sleeps until Retry-After before the next attempt (responses_retry.rs, no cap). Over WebSocket it first reconnects five times and falls back to HTTPS.
  • On a real 0.1.6-nightly.20261031.3956 server, a turn sat at retry 1/5 for 38½ hours until it was stopped. Stopping records an interruption and a server restart cancels the run; neither shows Limited.
Before (main 01a93e7b51)
Working for 5m 58s, Retrying provider (1/5)

Impact

Major degradation or frequent failure

Version or commit

main 01a93e7b51; also 0.1.6-nightly.20261031.3956

Environment

Linux x64, Node 26.12.1, web client. Codex CLI 0.166.0 against a local CLIProxyAPI (dev, 7cad1cb7) whose two Codex credentials point at a mock upstream returning OpenAI's usage_limit_reached.

Logs or stack traces

Retry item stored by main (statev2.sqlite):

{"status":"running","failure":{"class":"transport_error","message":"exceeded retry limit, last status: 429 Too Many Requests","code":"responseStreamDisconnected","retryable":true},"retry":{"attempt":1,"maxAttempts":5,"retryDelayMs":null}}

Workaround

Stop the turn and resume it after the reset. Separately, CLIProxyAPI can answer such pools with usage_limit_reached: router-for-me/CLIProxyAPI#7184 and #6647. Codex then stops at once, and current T3 shows Limited.

Proposed fix

In CodexAdapterV2:

  • classify a Codex retry whose error info carries HTTP 429 as usage_limit, the same rule fix(server): classify Codex 429 retry exhaustion as a usage limit #17501 applies to responseTooManyFailedAttempts;
  • if such a retry has not recovered after five minutes, interrupt the turn and report it failed on that usage limit. It then shows as Limited with Resume, and with "Resume at reset" when Codex has reported a reset time.

Retries that recover, and a user's Stop, keep their current outcome. No automatic resume is added (#16289).

Branch: ntindle:t3/codex-429-retry-limited. It changes the adapter and its replay tests:

  • a stop after the grace period;
  • no stop when the retry recovers within it;
  • Stop during the wait stays an interruption;
  • the existing 5xx retry case stays transport_error.
After (same setup, branch)
Limited, Usage limit reached, Reset time unavailable; retry manually

I'll open the focused PR once the intended behavior is triaged.

Activity

  1. samanyudas commented on Nov 2, 2026

    @samanyudas

    Note

    🤖 Claude Opus 5.5 responding on behalf of samanyudas

    I reproduced this on a real setup and got the same failure record. It also shows a case the proposed fix should cover: redeeming a reset upstream does not end the wait.

    • Versions: T3 Code desktop 0.1.7-nightly.20261102.4061, Codex CLI 0.166.0, CLIProxyAPI 8.0.22 (67465884), model gpt-6.1-sol.
    • The only Codex credential for that model hit its weekly limit (limit_window_minutes: 10080). CLIProxyAPI set a cooldown of about 59 hours.
    • Codex reconnected 5 times over WebSocket and fell back to HTTPS. It then sent responseStreamDisconnected with httpStatusCode: 429 and willRetry: true, and went silent. T3 stored the same transport_error retry item quoted above and kept the run running.
    • The user then redeemed a reset for that account. CLIProxyAPI kept its in-memory cooldown and kept answering 429 until we turned the credential off and on with PATCH /v0/management/auth-files/status. After that, a direct request through the proxy returned 200.
    • The waiting Codex turn still sent nothing for the next 6 minutes, 20 minutes after the 429 in total. Only Stop and a resend worked.

    The proposed behavior, stopping after a grace period and showing Limited with Resume, would also fix this case. The user could resume right after a reset instead of waiting for a stale Retry-After.

  2. ntindle commented on Nov 3, 2026

    @ntindle
    ContributorAuthor

    Note

    Claude Opus 5.5 in Claude Code, posting for Nick.

    An update on the fix. I'm not opening the five-minute stop proposed above, because #29208 already bounds a stuck provider retry. It stops the turn after 10 minutes and settles it with the retry's own failure.

    For this report, that failure is the remaining problem. Codex reports each retry as responseStreamDisconnected with the attempt's HTTP status, and T3 files a 429 there as a transport error. So a turn stopped this way shows Provider error, not Limited.

    #30376 classifies a 429 retry as a usage limit, the rule #17501 applies to exhaustion. I ran the setup above with each change merged locally into main 1a3f76e80a:

    Screenshots are in #30376.

    For the redeemed-reset case reported above, I expect the same, though I didn't run Resume. The turn still waits out #29208's 10 minutes. After that it is Limited, and Resume sends it again without waiting out the stale Retry-After.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions