Skip to content

OpenAI

A $200 Codex weekly limit burned in a median 23 hours

10 min read

Written by an AI model from the linked sources and checked against them by a second model. How we work

All about: Coding agents →
Image: relux.works

The Relux team went through 3,808 of its own Codex sessions and found that an agent waiting on other work burns input tokens at 83 to 188 million an hour once goal mode is on. In a September 11 write-up on the Relux blog, it traces the cost to a loop. Goal mode restarts the model as soon as a turn ends, the tool that would let the model pause is handed to only one model, and every check rereads the whole transcript.

The write-up builds everything on one formula: the cost of waiting is the number of checks times the size of the context. A model keeps no memory between steps, so each step resends the full transcript. At 240 thousand tokens of context, even tail -n 18 costs 240 thousand input tokens.

What an hour of waiting costs

The source session was an orchestrator, an agent that hands tasks to child agents in separate branches and then waits for them. It ran muse-spark 1.3 through the opencode provider. In 48 minutes of goal mode it made 173 continuations and 466 model calls, each rereading 120 to 470 thousand tokens, until the provider returned 429. The total came to 149 million input tokens, or 188 million an hour, at 590 calls an hour. Of the session’s 181 turns, 135 were exactly one polling command.

The other measured cases form a ladder, ordered by how long the model could pause inside one call:

  • Session A, gpt-5.6-sol: waited 7.8 hours, spent 650 million input tokens, 271 steps an hour, 83 million an hour. Each call waited 30 seconds, the ceiling for a running command.
  • Session B: waited 92 hours, spent 1.85 billion, 43 steps an hour, 20 million an hour. The model kept reading output through an empty write_stdin, which can block for up to five minutes, and ran sleep 30 in the shell 61 times.
  • Two goal sessions on gpt-6-astra, the one model that gets the pause tool: it slept 45 seconds per call, 60 and 129 times, and an hour of waiting cost 10 to 15 million at 55 to 100 calls.
  • The author’s estimate for notification-based waiting at the same context, one or two steps per event: about half a million tokens an hour.

Sleep made gpt-6-astra 6 to 18 times cheaper than the spin, yet it was still 20 to 30 times more expensive than a notification. Goal mode kept restarting it after every turn, 600 and 443 times in the two sessions, and it never slept for long. The write-up’s line on that: “Sleep lowers the price of the loop but doesn’t remove the loop.”

Prompt caching doesn’t change the picture. In the archive 97.8% of input came from the cache, and OpenAI bills cached input at 10% of the regular rate. The subscription, though, runs on a share of a sliding window, and the formula that turns tokens into that share isn’t published: the client receives only a percentage and a reset time. The author first argued that cached tokens count in full against the plan. After a Reddit discussion he struck that out, because OpenAI’s rule applies to per-minute API rate limits. What remains is the empirical part: sessions with 98% cache hits still emptied the weekly window.

Why the loop never slows down

Goal mode’s handler for an idle thread starts a new “continue working toward the goal” turn at once, with no backoff and no counter of empty continuations, and the per-goal token budget is unset by default. In the source session the gap between one turn ending and the next starting was 0.02 to 0.05 seconds, median 0.03. A typical turn took ten seconds, enough to read the context, run one poll and write “no changes”. Polling frequency was set by model latency and nothing else.

The continuation prompt, continuation.md, closes the remaining exit. Waiting only counts as progress if the model polled a live process in that turn. The model can mark a goal blocked only after three identical blockers in a row, and waiting for child processes isn’t a blocker. Only a human can pause a goal. When the author asked the model at 23:50 why it checked so often, it replied, in the author’s translation from Russian: “The goal contract requires every turn to either move the work or prove a verified wait by live-polling the runs.” It promised to poll less often. Goal mode launched the next turn 0.3 seconds after that answer.

Codex does have the right primitive. clock.sleep sleeps for up to 12 hours and wakes early on new input. It shipped in 0.141.0 on June 18 and has carried that name since 0.143.0 on July 8. It is registered only when the model’s catalog entry declares clock support, and in the built-in catalog only gpt-6-astra does. gpt-5.6-sol, gpt-5.5, gpt-5.4 and every model on a custom provider get an empty list, even though codex features list reports sleep_tool as stable. Across the archive, sleep was called 420 times, 418 of them by gpt-6-astra. Goal mode is on for every model, and the tool that keeps it from turning into a spin is on for one.

The muse-spark session lost the shell’s slower paths as well, through a serialization bug. The model passed numbers in tool arguments as floats, 60000.0 instead of 60000, and Codex rejected them with errors like invalid type: floating point 60000.0, expected u64. Across every session on that model, 10 of 10 write_stdin calls and 23 of 25 calls with yield_time_ms failed. That fixed the shell timer at its 10-second default and left the five-minute empty read out of reach. Its polls read the child’s event log and came back within a second.

Empty turns are the bare version of the same trap: the model writes “waiting”, gets restarted, does nothing again. In one session fourteen empty turns cost 77 million input tokens. And the pattern doesn’t depend on the provider: Qwen 3.6 on local llama.cpp spent 21 of the 22 turns in its goal session on pure polling.

Half the tokens in 52 sessions

The archive covers every rollout from two machines for July through September, CLI versions 0.144 to 0.154: 59.2 billion input tokens and 129 million output tokens. Goal mode was on in 52 of the 3,808 sessions, and those ate 29.2 billion. The seven most expensive sessions, 1.8 to 5.7 billion each, all had goal mode on. The heaviest one without it came to 1.2 billion. One goal session contains a single three-hour turn of 71 million tokens with 439 tool calls, mostly tail on child logs.

For the $200 Pro account, the write-up’s summary says the weekly limit ran out fifteen times over the summer, a median 23 hours into the window. Its detailed count, taken from the limit snapshots inside each token_count event, is more granular. Over ten weeks the weekly window reached 99% thirteen times and 100% eleven times, and “You’ve hit your usage limit” appears on 28 calendar days. Reaching 99% took a median of 25 hours, 7 at the fastest and 93 at the slowest. The five-hour window ran out only twice. At 83 to 188 million an hour of waiting, a day of it comes to two to four billion input tokens.

Orchestrators get hit hardest. With one child process, a blocking wait is cheap: at five minutes per call, a three-hour process costs about 36 calls, 12 steps an hour instead of 270. With two or more, the parent can’t block on one and miss another crashing, so it falls back to a short loop over all of them. Codex’s own barrier, wait_agent, waits for whichever of several agents finishes first, but it only sees agents spawned through spawn_agent. Relux’s child processes were invisible to it.

How Claude Code’s /goal waits

Claude Code has a mode with the same name and a different dispatcher. Per its docs, a separate small model checks the goal condition after every turn. If a background command or subagent is still running, the check is deferred and the result arrives as a new turn. When background work drags on, the first check-in comes after 30 minutes, the interval then doubles, and there are at most three check-ins without a human. Several turns in a row without a tool call stop the loop with a warning. Bash and subagents have background modes that notify on completion, and Monitor wakes the model on each new output line. The /loop docs recommend it over timer polling because it uses fewer tokens.

Since Claude Code’s source is closed, the author checked the mechanics in ClawCodex, a third-party Python port. There, background.py waits for the process and queues a <task-notification> envelope, and the server hands it to the model as a single turn. The model isn’t called while the process runs. The difference is who waits: in Claude Code the wrapper waits and wakes the model, while in Codex a finished shell command never becomes a message, so the model has to go and look.

What to change

The proposed Codex fixes are each small: register clock.sleep whenever a thread has an active goal, or make always_on the default when goals are enabled; add a delay after a waiting or empty goal turn; accept 60000.0 where 60000 is expected; and name clock.sleep and a long write_stdin in continuation.md as valid forms of verified waiting.

On the user side, the simplest floor for goal tasks is gpt-6-astra at minimal effort, the only model with sleep out of the box. On Pro, that is a tier OpenAI had stopped selling to new subscribers by the time the write-up appeared. For other models, the flag goes in ~/.codex/config.toml as sleep_tool = { mode = "always_on" } under [features], and after a restart the tool is registered for any model. The author’s caveats: those models weren’t trained to use it and may keep polling or sleep instead of working, and on providers that emit floats it won’t work at all, because duration_ms is an integer too. Relux is also teaching its orchestrator to wait on all children with one call and putting a budget on every goal, a path it says it hasn’t verified at scale. The tables can be reproduced with codex-rollout-audit, a single dependency-free Python file that reads ~/.codex/sessions locally.

“Until the model is given a way to wait without spending a step, the one who waits is the one who pays,” the write-up concludes.

No comments yet. Be the first.

Join the conversation

Sign in to leave a comment. It appears after moderation.