Repository navigation
Description
Summary
On a large Claude data directory, every cold ccusage statusline render walks the entire history twice and peaks at ~4 GB RSS. With several Claude Code sessions open, the renders overlap and the machine ends up in swap.
The active session block query is the main offender: it is issued with no date bounds at all, so it loads every entry ever recorded just to find the current 5 hour block.
Environment
- ccusage 20.0.24, prebuilt
@ccusage/ccusage-darwin-arm64binary - macOS on Apple Silicon (M2 Max, 12 cores, 96 GB)
~/.claude/projects: 12.7 GB across 8,495 JSONL files, 8.4 GB of it written in the last 7 days- Invoked from the Claude Code
statusLinehook
Measurements
Same payload on stdin, /usr/bin/time -l:
| Run | Wall time | Peak RSS |
|---|---|---|
| Cold render, default flags | 9.47 s | 3.99 GB |
Cold render, --cost-source cc |
9.81 s | 3.91 GB |
Cache hit, --refresh-interval 600 |
0.16 s | 0.04 GB |
Two details make this worse in practice:
--refresh-intervaldefaults to 1 second, so in a normal Claude Code session almost every render is a cold render.- The cost is paid per session. With N sessions open there are N concurrent full scans, each allocating multiple GB.
Root cause
In rust/crates/ccusage/src/commands/mod.rs, render_statusline performs two loads:
let today_shared = statusline_today_shared(args, shared, utc_now());
let today_cost = load_entries(&today_shared, None)
.map(|entries| {
entries
.iter()
.filter(|entry| {
entry.date.replace('-', "") == today_shared.since.as_deref().unwrap_or_default()
})
.map(|entry| entry.cost)
.sum::<f64>()
})
.unwrap_or(0.0);
let blocks = load_entries(shared, None)
.map(|entries| identify_session_blocks(entries, DEFAULT_SESSION_DURATION_HOURS))
.unwrap_or_default();
let active_block = blocks.iter().find(|block| block.is_active && !block.is_gap);shared comes from statusline_shared, which builds SharedArgs::default() and only overrides offline and pricing_overrides. There is no since or until, so the block load is unbounded.
The today load does set since, but the filter runs after the entries are materialized, and the Claude adapter does not use since to skip files. In rust/adapters/claude/src/lib.rs, load_entries_inner discovers every usage file and reads all of them before deduplicating:
let files = usage_files(&paths, project_filter);
...
let loaded_files = read_files_parallel(&files, shared.single_thread, |file| {
read_usage_file(file, tz.as_ref(), mode, pricing.as_ref())
});Every entry from every file is then collected into a single Vec<LoadedEntry>, which is what drives peak RSS on a multi GB history.
The Codex adapter already carries the idea of date-bounded loading (has_date_bounds in rust/adapters/codex/src/loader.rs), added when date bounded Codex reports were scanning all historical session files in #1598. The Claude path has no equivalent.
Suggested fix
I am happy to open a PR. Rough plan, in order of impact:
-
Bound the block query. The active block only needs entries from
now - DEFAULT_SESSION_DURATION_HOURS, plus a margin so gap detection stays correct. Astatusline_block_sharedhelper mirroring the existingstatusline_today_sharedkeeps the change local tocommands/mod.rs. -
Prune files by mtime in the Claude adapter when
sinceis set. Session JSONL files are append only, so a file whose mtime predates the lower bound cannot contain entries inside the window. A conservative margin covers clock skew and late writes. This mirrors the Codex behavior and turns the common case from 8,495 files into the handful touched today. -
Fold instead of collect for statusline. Statusline consumes aggregates, not entries. Exposing a streaming variant of
load_entriesthat accepts a visitor, or returning per file summaries, keeps peak RSS bounded regardless of history size. This also helps the reports that already aggregate immediately after loading. -
Reconsider the statusline
--refresh-intervaldefault. At 1 second the cache is bypassed on essentially every render. A default in the tens of seconds, still invalidated by transcript mtime as it is today, would make the cache effective out of the box. Raising it locally took repeat renders from 9.5 s to 0.16 s.
Items 1 and 2 are the ones that matter for users with large histories. Item 3 is the structural fix. Item 4 is a one line default change that helps everyone immediately.
Related closed issues that touched the same class of problem on other adapters: #821, #885, #1124, #1598.
Activity
pullfrog commentedon Sep 23, 2026
github-actions commentedon Sep 23, 2026
Pullfrog triage: priority:medium
Kind: bug
Maintenance fit: maintainable
Confidence: high
Cold statusline render loads full Claude history twice for today cost and unbounded active-block query, measured at 9.5s and 4GB RSS. Code confirms unbounded load_entries in render_statusline. This is a performance defect in existing behavior with bounded fix mirroring Codex date bounds, not a duplicate of closed broader reports.
Decision: kept open.
perf(claude): skip stale files for date-bounded loads and bound statu…