high-frequency code-mode polling, amplified by a very large resumed context
_After consiming unusual huge tokens usage with a Codex Sol 5.6 ~600mln tokens for a day vs usual 150-200mln on a same code base and same sessions from Codex 5.5 i passed a logs to a Codes to understand if this is not my own WSL bug fix (https://github.com/openai/codex/issues/16690) that force me to build a codex-cli every wheek with latest upstream fixes._
And Codex 5.6 Sol xHigh found a possible culprit (maybe this is a known issue?):
During the final Medium interval:
- 43 minutes, 2 user messages
- 292 model responses
- 96 exec calls and 192 wait calls
- All 192 waits explicitly used yield_time_ms: 1000
- 49.39M raw tokens
- Only 10,828 reasoning tokens
- Five-hour usage increased from 58% to 100%
- Cache ratio was healthy at 98.3%
The CLI documents a 10-second default (codex-rs/core/src/tools/code_mode/wait_spec.rs:13), but accepts the model’s one-second value unchanged (codex-rs/core/src/tools/code_mode/wait_handler.rs:92). There is no minimum clamp. Every poll causes another model response with approximately 170K–240K input tokens.
Other Findings
The parent rollout now totals 1.153B raw tokens, with 95.4% cached. Most historical usage was GPT-5.5 xhigh (967M), but Sol added 268M on July 10 alone.
Secondary contributors:
- One guardian auto-review used 60,882 additional raw tokens. approvals_reviewer = "auto_review" is enabled.
- Historical view_image outputs occupy 132.6 MB, including 48 outputs over 1 MB.
- Multi-agent is enabled but no actual spawn_agent activity appears.
- There were no code-mode-host spawn failures in the parent thread.
- No goals or automatic goal continuations were detected.
On time of investigation upstream still don't have a fixes.
10 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
I can confirm a very similar pattern on Codex app on Windows, mainly using GPT-5.6 Sol with medium reasoning.
An otherwise ordinary component task reached 117,961,931 raw tokens across 971 model responses while running tests. It made 683
waitcalls, including 305 withyield_time_ms: 1000. The input context had a median size of ~129K tokens and reached 217K, with ~99% cache utilization, while producing only ~96K output tokens and ~22K reasoning tokens.This does not appear to be isolated. In my July 9–11 local rollouts:
waitcalls accounted for 458.7M raw tokens and 3,901 model responses.During full e2e suites lasting 7–9 minutes, I observed 35–42 polling rounds per run, generally using 10-second waits. Later targeted runs frequently used one-second waits. Each completed wait appears to cause another model request with a large, mostly unchanged context.
For additional context, from July 1–8 only 3 of 22 active threads exceeded 10M raw tokens, and those were genuinely large, highly active tasks. From July 9–11, 8 of 16 exceeded 10M, including otherwise ordinary tasks spending time waiting for long-running commands.
Sampled some usage i have done with 5.5 and after 5.6sol, seems like this existing problem amplified with 1M Context size increase.
<img width="396" height="396" alt="Image" src="https://github.com/user-attachments/assets/936e86ef-ce40-4de1-be20-f74d1354bf1e" />
It seems like openai team also on track, after these changes they have done i will do the same analyses again. Context size change probably lowered the impact but not an solution.
<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="en" dir="ltr">Updates for Codex and ChatGPT Work users. No nerfing, only good stuff!<br><br>- We have landed inference optimizations and are passing down savings to all the subscriptions for GPT-5.6 Sol. That should result in around 10% more usage on its own.<br>- We noticed that by changing the… <a href="https://t.co/ej7DdJeyBL">https://t.co/ej7DdJeyBL</a></p>— Tibo (@thsottiaux) <a href="https://x.com/thsottiaux/status/2076495156757577895?ref_src=twsrc%5Etfw">July 13, 2026</a></blockquote>
Analysis (community)
Residual analysis for #32309 (code_mode
waitpolling re-bills the full transcript every yield) under docs/contributing.md — invitation-only. No unsolicited PR.Root-cause hypothesis
The code_mode
waittool has no minimum clamp at any layer onyield_time_ms, and every yield re-enters the model with the full growing prefix, so a long-running exec cell becomesN ≈ ceil(runtime / yield_time_ms)billable sampling requests — the 1s-poll variant of the re-entry mechanism.@ssh4net — your forensic work here is the sharpest trace of this burn I've seen: 192 waits at
yield_time_ms: 1000, 170K–240K input tokens per re-sample, and the 1.153B raw-token parent rollup isolate the mechanism precisely. I want to add the missing piece: the full re-entry chain fromwait_spec.rsthrough the turn layer, plus a code_mode-specific fix outline that thewait_agentanalysis on #35259 explicitly did not cover.What #35259 covered vs what this is. The analysis posted on #35259 traced
multi_agents_v2/wait_agent(codex-rs/core/src/tools/handlers/multi_agents_v2/wait.rs:178-196) — a different handler that polls a subagent mailbox viaInputQueue::subscribe_activity, with a 30s default / 10s floor / 1h max, and out-of-rangetimeout_msrejected atwait.rs:54-63. That post explicitly noted its fix "doesn't cover code_modewaitor explicit short choices." This is that uncovered path. The code_modewaittool (codex-rs/core/src/tools/code_mode/wait_spec.rs+wait_handler.rs, tool name"wait") polls an exec cell (process stdout), uses argyield_time_ms(nottimeout_ms), defaults to 10s (not 30s —code-mode-protocol/src/runtime.rs:12DEFAULT_WAIT_YIELD_TIME_MS), and has no config knob (the default is a flatconst; compareexec'sBUFFERED_EXEC_YIELD_TIME_MSoverride atcore/src/tools/code_mode/mod.rs:57).The no-min-clamp finding, confirmed and extended. You already cited
wait_spec.rsand stated "there is no minimum clamp." Confirmed at HEAD4c43465133, with the structural reason and the full chain:JsonSchemastruct (codex-rs/tools/src/json_schema.rs:41-74) has nominimum/maximumfields;JsonSchema::number()emits only{"type":"number","description":...}. A clamp cannot be expressed in the tool schema the model sees.wait_handler.rs:94copiesyield_time_ms: args.yield_time_msstraight intoWaitRequestwith zero comparison.code-mode/src/service.rs:35-42yield_timeout(): the +1s grace (from #33867) applies only foryield_time_ms >= 10000(MIN_YIELD_TIME_FOR_GRACE); any sub-10s value returns the rawDuration::from_millis(value), which reachescell_actor/mod.rs:569tokio::time::sleep(duration)— the cell sleeps exactly 1s then yields.waitreturns an ordinaryFunctionToolOutput(code_mode/mod.rs:213, success flagSome(true)).stream_events_utils.rs:324setsneeds_follow_up = true;turn.rs:2199aggregates it andturn.rs:2369-2371fires onend_turn=false; the loop re-invokesrun_sampling_requestwithsess.clone_history().for_prompt(...)(turn.rs:295-301) — the model re-reads the entire growing transcript of priorwaitoutputs just to emit the next 1swaitcall. That is the 170K–240K per poll.So:
N = ceil(runtime / yield_time_ms)billable re-samples, each billing the full prefix. Withyield_time_ms=1000, roughly one full-prefix inference per second for the life of the cell.Ranked fix outline (all OSS):
yield_time_msminimum clamp inwait_handler.rs(e.g. floor 5000ms, mirroringunified_exec's yield floor). Smallest diff, directly closes the 1s vector; the clamp must live at the handler layer because theJsonSchematype cannot expressminimum.code_mode_service.wait(WaitRequest{cell_id, yield_time_ms})internally onYieldedwhile the cell is still running, without returning to the model — the code_mode analogue of thewait_agentre-arm proposed on #35259. Collapses N polling turns into ≤1.waitthe same treatmentexecgot in #34441 (BUFFERED_EXEC_YIELD_TIME_MSoverride applies to exec only,code_mode/mod.rs:105-109): either honor the buffered-exec yield floor or add a documentedwaitfloor constant.waitandwait_agentfor model routing; related, #33669 documents thatmodel_info.tool_mode = Some(Direct)silently overrides enabled CodeMode flags (tools/mod.rs:64-74,unwrap_or_else) with no warning.Related cluster: #32640 observed this same burn empirically but attributed it to
multi_agent_v2— the params{cell_id, yield_time_ms, max_tokens}in its logs are the code_modewaitsignature, notwait_agent; #29122 (code_mode silent activation); #33669 (tool_mode=direct override).Fail-first tests I'd want before any merge: (1)
yield_time_ms=1000is clamped to the floor in theWaitRequestthe service receives (today: passes 1000 through unchanged); (2) a 10-min cell yielding every 1s produces ≤1run_sampling_requestfor the whole wait (today: ~600); (3)yield_timeout(1000)returns ≥ the floor (today: returns raw 1000ms). Happy to draft the test patch if a maintainer wants it.Happy to refine measurements or the fail-first tests if maintainers want them. No unsolicited PR from me.
Trying to find relevant issue here. But I still experience codex has a strong preference for ending up in polling loops, sometimes for no reason. @1.46.0
<img width="428" height="1738" alt="Image" src="https://github.com/user-attachments/assets/77e6280e-c2ca-4c6a-be82-ca9831b1a171" />
This is one of the more obvious one, but codex will often end up in polling loops for longer builds, checking test status in other machines etc.
This eat tokens like crazy, I assume. I do feel like I run out very quickly when this problems happens.
@jkammerland — to place your 1.46.0 case correctly, the params matter, because two different handlers produce near-identical symptoms. If the tool call is
waitwith{cell_id, yield_time_ms, ...}it is the code_mode path traced above. If it iswait_agentwithtimeout_msit is the multi-agent path, which already has a 10s floor and rejects out-of-range values (codex-rs/core/src/tools/handlers/multi_agents_v2/wait.rs:54-63). #32640 hit this same burn and attributed the code_mode signature tomulti_agent_v2, so the distinction is worth nailing down before more measurements pile up.For the code_mode path nothing has changed on current
main:codex-rs/core/src/tools/code_mode/wait_handler.rs:106still copiesyield_time_ms: args.yield_time_msintoWaitRequestwith no comparison, so a model-chosen 1s yield still reachestokio::time::sleepunclamped and re-bills the entire growing prefix roughly once per second. Fix (a) above — a floor at the handler layer, which is where it has to live sinceJsonSchemacannot expressminimum(codex-rs/tools/src/json_schema.rs:41-74) — remains the smallest diff that closes the 1s vector. Happy to draft the fail-first tests if a maintainer wants them.I replayed four long-lived Windows Desktop parent rollouts (Aug 7-12, Asia/Bangkok) against several circuit-breaker policies.
The sample contains about 27.6k outer
execcalls and 7.4k outerwaitcalls; 26.7kexeccells contained exactly one nested tool, while only about 700 contained multiple tools. There were 349 wait bursts, 113 longer than 10 calls, with a maximum of 500.A third-wait boundary is the better default: blocking/coalescing the second wait catches only about 941 additional historical calls but also affects all 44 ordinary two-wait bursts. An adaptive "second only for <=5s waits" policy adds just 22 catches over the third-wait policy. A separate cap of 20 same nested-tool roundtrips per 10 minutes would have intercepted about 7.7k calls; cap 40 only about 1.2k.
There is an important mitigation gap on current main (
0e82c62a449c484edc053fa3ff82410870df0145):CodeModeWaitHandler::pre_tool_use_payload()explicitly returnsNone, andCodeModeExecuteHandlerreceives a custom payload, so the default function-toolPreToolUsecontract does not apply. Therefore user hooks cannot guard the outerexec/waitmodel-roundtrip boundary; they can only see many nested tools. A local nested-tool cap helps because more than 96% of sampled exec cells were single-tool cells, but it cannot stop a pure outer wait loop.Suggested runtime fix:
PreToolUse.This complements the existing minimum-clamp proposal and avoids penalizing legitimate two-stage waits.
Analysis (community)
@kirillsaven — the Windows Desktop parent-rollout sample (27.6k outer
exec, 7.4k outerwait, many 1-nested exec cells) is useful because it separates code_modewaitfrom multi-agentwait_agent.Root-cause hypothesis (re-checked 2026-08-20 at
main3b45c2906): the two handlers are not equivalent on timeout policy.waitstill has no minimum clamp.yield_time_msis deserialized and passed straight intoWaitRequest(codex-rs/core/src/tools/code_mode/wait_handler.rs:27-28,104-106). The tool spec documents a default of 10000 ms (wait_spec.rs:13-16) but does not enforce a floor. Shortyield_time_ms(e.g. 1000, as in earlier reports on this issue) still re-enters the model with the growing prefix.wait/wait_agentdoes clamp (codex-rs/core/src/tools/handlers/multi_agents/wait.rsms.clamp(MIN_WAIT_TIMEOUT_MS, MAX_WAIT_TIMEOUT_MS); v2 path also emits a clamped-timeout message).If the 7.4k outer waits in your sample are code_mode
wait(cell_id+yield_time_ms), they sit on the unclamped path this issue tracks. If they arewait_agent(timeout_mson a child agent), they are the D/#35259 family (local timeout → follow-up turn), not a missing yield clamp.I am not treating the 27.6k
execvolume as a defect by itself — only the wait/re-entry policy. No unsolicited PR. A fail-first test for this issue is: code_modewaitwithyield_time_ms: 1is raised to a documented minimum beforecode_mode_service.wait.Issue anchor remains #32309. Happy to split a table of wait-tool names vs clamp behavior if that helps triage incoming Desktop samples.
@bbingz Thanks for calling out the handler distinction. I rechecked the retained replay script and need to correct the interpretation of my earlier aggregate.
Its
WAIT_TOOLSset combineswait,wait_agent,wait_threads, andcodex_app__wait_threads. Both the aggregate labelledouter_wait_calls_bypass_pretooluseand the wait-burst calculation use that combined set. That label was too broad: the aggregate is not valid evidence that every counted wait uses the unclamped code_mode path.A fresh run over the four currently retained parent journals, with the script's explicit window [2026-08-07, 2026-08-13) at UTC+7, gives this tool-name split:
| Tool name | Calls |
|---|---:|
| wait | 3,418 |
| wait_agent | 384 |
| wait_threads / codex_app__wait_threads | 0 |
| exec | 14,591 |
This does not reproduce the approximately 7.4k waits / 27.6k execs in my previous comment. I cannot reconcile that earlier total from the currently retained inputs, so please do not use it as a code_mode-only denominator or as a verified estimate of billing savings. The earlier second/third-wait comparisons were also based on the combined wait set and should not be treated as handler-specific measurements.
The narrower runtime point is separate: the code_mode wait handler's absent PreToolUse payload in the pinned source I linked means a nested-tool hook cannot guard that outer model-follow-up boundary. I agree that code_mode yield clamping and multi-agent timeout-to-follow-up behavior need separate tests/issues. A minimum clamp plus output/progress-aware wait coalescing is the relevant code_mode proposal; raw exec volume alone is not proof of a defect.
No new claim about current-main clamp behavior is being made here, and no unsolicited PR is being opened.