TUI blocks all models when primary bucket hits 100%, even models that are not actually rate-limited

Open 💬 3 comments Opened May 17, 2026 by Gerry9000
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Summary

When a long-running codex TUI session sees rate_limits.primary.used_percent reach 100% (regardless of which model triggered it), the session refuses to send further requests on any model — including models the server is not actually blocking, like gpt-5.3-codex-spark on a prolite plan.

A fresh codex session started in a new terminal works fine on the same model that the affected terminal refuses to use. The web portal correctly shows separate usage stats for spark vs. the main models, but the CLI's rate-limit envelope conflates them into a single limit_id: "codex" bucket.

Reproduction

  1. Start codex on gpt-5.5 (or whatever the default primary model is on a prolite plan)
  2. Burn through the 5-hour primary bucket until rate-limits return 100% on the primary window
  3. In a separate codex terminal already running gpt-5.3-codex-spark, attempt to send a message
  4. The spark terminal refuses to send, even though server-side spark is not blocked
  5. Open a brand-new terminal, launch codex, switch to gpt-5.3-codex-spark → works immediately on the same account

Expected Behavior

The TUI should use rate_limit_reached_type (the explicit server signal) to decide whether to block a request, not primary.used_percent alone. When rate_limit_reached_type: null, the server is permitting the request and the client should send it.

Alternatively, the rate-limit envelope should be enriched server-side to distinguish per-model availability the way the web portal does.

Evidence

I inspected a past spark session from a prolite account where rate_limits.primary.used_percent was 100% the entire time:

{
  "limit_id": "codex",
  "primary": {
    "used_percent": 100.0,
    "window_minutes": 300,
    "resets_at": ...
  },
  "secondary": { "used_percent": 62.0, "window_minutes": 10080, ... },
  "credits": { "has_credits": false, "balance": "0" },
  "plan_type": "prolite",
  "rate_limit_reached_type": null
}

That session logged 2275 successful response events at primary=100%. The server allows spark at 100% primary — the field rate_limit_reached_type: null confirms it.

Impact

Users hitting the primary bucket lose access to spark in any pre-existing terminal even though their spark allowance is unused. They must quit and restart codex to recover access. For long-running terminal sessions (tmux panes, persistent agents), this is a forced session loss with no workaround inside the running TUI.

Environment

  • codex-cli 0.130.0
  • Plan: prolite
  • Default model: gpt-5.5
  • Affected model when this happens: gpt-5.3-codex-spark

Suggested Fix

In the TUI's request gate, change the decision from:

if rate_limits.primary.used_percent >= 100: block_request()

to:

if rate_limits.rate_limit_reached_type is not None: block_request()

The used_percent: 100 value is informational (drives the UI meter), not authoritative for whether a specific request will be allowed. The authoritative signal is rate_limit_reached_type.

View original on GitHub ↗

3 Comments

github-actions[bot] contributor · 3 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #23150

Powered by Codex Action

Gerry9000 · 3 months ago

Why this is not a duplicate of #23150

These bugs are related but operate at different layers, and only one of them explains what users actually observe in the TUI:

#23150 is server-side accounting: spark calls being recorded against the codex bucket meter in addition to the codex_bengalfox bucket. It's a telemetry/visibility issue — the numbers shown to the user are misleading, but the server's actual block decisions are independent of those numbers.

#23171 (this issue) is client-side decision logic: the TUI refuses to send requests for any model when primary.used_percent hits 100, even when the server explicitly signals no block is in effect (rate_limit_reached_type: null). The block decision happens before any network call to the server.

The empirical proof these are independent:

  1. Fresh terminal works at primary=100% codex bucket. Launching codex in a new terminal and using gpt-5.3-codex-spark succeeds immediately while an existing terminal on the same model on the same account refuses. If #23150's cross-bucket draining caused real server-side blocking of spark, the new terminal would fail too. It doesn't.
  1. Past spark sessions ran successfully at primary=100%. Session 019e2d84-604c-7493-897c-ab5d7cd1dd05 (started 2026-05-15T21:21:50) logged 2275 successful response events with primary.used_percent: 100.0 and rate_limit_reached_type: null throughout. The server actively let spark through at 100% primary.
  1. Web portal confirms the bucket numbers match what the CLI sees. On a prolite account, the portal shows "38% remaining weekly" and the CLI's secondary.used_percent reads exactly 62.0. The buckets that the CLI sees are the same buckets the user sees in the portal.
  1. Resuming an old spark session reproduces the bug deterministically — and proves the block is purely client-side. A spark-dominated session (id 019e3346-899c-7be2-8d37-ebdb916a26a4) had last received a rate-limit envelope 11 hours earlier (primary: 100%, secondary: 31%, rate_limit_reached_type: null). I quit the TUI, resumed the session, and sent a new message — the TUI returned an "out of tokens" error. The session log shows:
  • task_started event at 2026-05-17T17:11:36.690Z
  • NO subsequent rate_limits envelope
  • NO error response from the server
  • NO further events at all in the rollout file

The TUI gated on the 11-hour-old stored primary.used_percent: 100 from the session file and refused to send the request to the server. No API call was made, no fresh envelope received. The actual account state at that moment was secondary: 62% (per a live concurrent session 019e36e1-0778-7091-b692-3e834df3fd21 and per the web portal) — but the resumed session was making its decision from secondary: 31% stored state, which proves the gating logic is operating on cached/persisted local data, not on a fresh server check.

Fixing #23150 (correcting cross-bucket accounting) would clean up the displayed numbers but would NOT fix this bug. As long as the TUI gates on used_percent >= 100 from cached/persisted state instead of rate_limit_reached_type from a fresh server poll, existing sessions and resumed sessions will continue to lock out users from models the server is happy to serve.

Note on CLI version: my evidence is from codex-cli 0.130.0, which surfaces only limit_id: "codex" and limit_id: "premium". The codex_bengalfox bucket #23150 references appears starting in 0.131.0-alpha.9. The bug I'm describing predates that version.

Happy to provide the relevant rollout files privately if it would help debugging.

Gerry9000 · 3 months ago

Honest correction to the diagnosis above

After more inspection, I have to walk back some of my claims about exactly where the gate fires.

I had hypothesized that the TUI gates on the rollout file's most recent event_msg/token_count event. But inspecting a different locked session (29MB, 31 user messages, 1631 assistant turns) shows the last token_count was already primary.used_percent: 0.0, rate_limit_reached_type: null — yet the session was still exhibiting the lock pattern (sub-second task_complete after task_started, no agent_message, no error event written). The same session also has a successful 6-second task interleaved between two failed sub-second ones, with no new token_count events between them.

That rules out "TUI gates on the rollout's last token_count" as a complete explanation. The gate is more likely driven by:

  • An in-memory rate-limit state that updates from server responses but isn't fully reflected in the rollout file, OR
  • A separate persistence layer outside the rollout

What I can still defend:

  • The lock symptom is real and reproducible (sub-second task_complete with no model output, no rate-limit envelope, no error response in the session log)
  • Fresh terminals are not affected — they can use the same models on the same account at the same moment
  • The server's rate_limit_reached_type is null when these locks fire (confirmed by adjacent live sessions polling the same account)

What I cannot defend without further testing:

  • The rollout-edit "workaround" worked on one session, but I cannot isolate whether the edit was causal or whether the act of quit-and-resume itself cleared some other state. The test would require reproducing the lock on a fresh-but-locked session and running both a control resume (no edit) and a treatment resume (with edit). I'm unable to run that test now because the 5h primary bucket has since reset.

The core ask in this issue still stands: the TUI is gating against models the server is willing to serve, and the gate's authoritative signal should be rate_limit_reached_type, not used_percent. I'll update with a cleaner reproduction the next time the bucket fills.