Bug Report: Usage limits stay hidden after launching Codex offline and reconnecting
What version of the Codex App are you using (From “About Codex” dialog)?
Version 26.602.40724
What subscription do you have?
Pro x20
What platform is your computer?
Darwin 25.5.0 arm64 arm
What issue are you seeing?
If Codex is launched while the machine is offline, the usage/limits data in the menu/status UI never appears after reconnecting to the internet. The limits only show again after restarting Codex.
This appears to be a missing retry/lazy-fetch path for account rate limits after the startup rate-limit prefetch fails.
What steps can reproduce the bug?
- Disconnect the machine from the internet.
- Launch Codex.
- Open the menu/status UI where usage limits normally appear.
- Reconnect to the internet while Codex remains running.
- Reopen the menu/status UI.
What is the expected behavior?
After connectivity is restored, Codex should retry loading account usage/rate-limit data and show current usage limits without requiring a restart.
Ideally, any surface that displays usage limits should lazily fetch or refresh the data when:
- the account is ChatGPT-authenticated,
- the model provider requires OpenAI auth,
- no rate-limit snapshot is cached, or the cached snapshot is stale,
- and no rate-limit refresh is already in flight.
Additional information
Environment
- Codex version:
TODO: codex --version - OS:
TODO - Install method:
TODO: npm / Homebrew / release binary / other - Auth mode: ChatGPT account
- Network state at launch: offline
Suspected cause
This looks like a rate-limit cache warmup failure that never recovers.
From a quick code inspection, the TUI appears to perform a one-shot startup prefetch of account rate limits after app-server bootstrap:
// Kick off a non-blocking rate-limit prefetch so the first `/status`
// already has data, without delaying the initial frame render.
if requires_openai_auth && has_chatgpt_account {
app.refresh_rate_limits(&app_server, RateLimitRefreshOrigin::StartupPrefetch);
}
That prefetch goes through refresh_rate_limits, which spawns a background task and sends AppEvent::RateLimitsLoaded { origin, result } when account/rateLimits/read completes:
pub(super) fn refresh_rate_limits(
&mut self,
app_server: &AppServerSession,
origin: RateLimitRefreshOrigin,
) {
let request_handle = app_server.request_handle();
let app_event_tx = self.app_event_tx.clone();
tokio::spawn(async move {
let result = fetch_account_rate_limits(request_handle)
.await
.map_err(|err| err.to_string());
app_event_tx.send(AppEvent::RateLimitsLoaded { origin, result });
});
}
The actual account limits RPC is:
let response: GetAccountRateLimitsResponse = request_handle
.request_typed(ClientRequest::GetAccountRateLimits {
request_id,
params: None,
})
.await
.wrap_err("account/rateLimits/read failed in TUI")?;
The RateLimitRefreshOrigin docs are also relevant:
/// A `StartupPrefetch` fires once, concurrently with the rest of TUI init, and
/// only updates the cached snapshots (no status card to finalize). A
/// `StatusCommand` is tied to a specific `/status` invocation and must call
/// `finish_status_rate_limit_refresh` when done so the card stops showing a
/// "refreshing" state.
pub(crate) enum RateLimitRefreshOrigin {
StartupPrefetch,
StatusCommand { request_id: u64 },
}
So if Codex starts offline, the startup account/rateLimits/read call fails. Since StartupPrefetch is only a cache warmup, the UI starts with no cached rate-limit snapshots.
The part that looks like the likely bug is that the old or expected “poller” path appears to be effectively disabled/no-op:
pub(super) fn stop_rate_limit_poller(&mut self) {}
#[cfg_attr(not(test), allow(dead_code))]
pub(super) fn prefetch_rate_limits(&mut self) {
self.stop_rate_limit_poller();
}
Meanwhile the status/menu surfaces appear to render from the cached rate_limit_snapshots_by_limit_id map rather than lazily fetching if the cache is empty. For /status, the code collects cached snapshots and passes them into the status output:
let rate_limit_snapshots: Vec<RateLimitSnapshotDisplay> = self
.rate_limit_snapshots_by_limit_id
.values()
.cloned()
.collect();
Reconnecting also does not obviously refill this cache. The app-server event handler updates the cache only when it receives AccountRateLimitsUpdated:
ServerNotification::AccountRateLimitsUpdated(notification) => {
self.chat_widget
.on_rolling_rate_limit_snapshot(notification.rate_limits.clone());
return;
}
But an AccountUpdated notification only updates account state / plan / ChatGPT-account status and does not trigger a rate-limit fetch:
ServerNotification::AccountUpdated(notification) => {
self.chat_widget.update_account_state(
status_account_display_from_auth_mode(
notification.auth_mode,
notification.plan_type,
),
notification.plan_type,
notification
.auth_mode
.is_some_and(AuthMode::has_chatgpt_account),
);
return;
}
This would explain the observed behavior:
- launch offline,
- startup prefetch fails,
- cache remains empty,
- menu/status surfaces render from the empty cache,
- reconnecting updates connectivity/account state but does not request limits,
- limits remain hidden until process restart causes startup prefetch to run again while online.
Possible fix
Add a retry or lazy-fetch path for rate limits after failed startup prefetch.
A minimal fix would be to trigger a rate-limit refresh when opening/rendering a surface that displays usage limits if:
should_prefetch_rate_limits()is true,rate_limit_snapshots_by_limit_idhas nocodexsnapshot, or the snapshot is stale,- and no rate-limit refresh is already in flight.
A cleaner implementation might add a new origin:
RateLimitRefreshOrigin::StatusSurfacePrefetch
This would behave like StartupPrefetch: update cached snapshots and refresh status surfaces, but not finalize a /status card.
Another option is bounded retry/backoff when StartupPrefetch fails with a connectivity/network error.
Regression test idea
Simulate this sequence:
- Start TUI with ChatGPT auth and OpenAI-auth-required provider.
- Make the initial
account/rateLimits/readstartup prefetch fail with a network error. - Verify no rate-limit snapshots are cached.
- Simulate connectivity restoration or opening the status/menu surface.
- Make the next
account/rateLimits/readsucceed. - Assert that the usage-limit rows appear without restarting Codex.
A narrower unit/integration test could verify that opening the status/menu surface with an empty rate-limit cache dispatches a refresh event when should_prefetch_rate_limits() is true.