Skip to content

statusline: active block query loads the full Claude history on every cold render (9.5 s, 4 GB RSS on a 12.7 GB data dir) #1790

Closed

Description

@alissonlinneker

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-arm64 binary
  • 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 statusLine hook

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:

  1. --refresh-interval defaults to 1 second, so in a normal Claude Code session almost every render is a cold render.
  2. 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:

  1. Bound the block query. The active block only needs entries from now - DEFAULT_SESSION_DURATION_HOURS, plus a margin so gap detection stays correct. A statusline_block_shared helper mirroring the existing statusline_today_shared keeps the change local to commands/mod.rs.

  2. Prune files by mtime in the Claude adapter when since is 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.

  3. Fold instead of collect for statusline. Statusline consumes aggregates, not entries. Exposing a streaming variant of load_entries that 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.

  4. Reconsider the statusline --refresh-interval default. 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

  1. pullfrog commented on Sep 23, 2026

    @pullfrog
    Contributor
  2. added
    bugSomething isn't working
    priority:highHigh user impact or an important regression.
    triage:maintainableFits the repository's maintainable scope and needs review.
    on Sep 23, 2026
  3. added
    priority:mediumA normal bug or meaningful improvement.
    and removed
    priority:highHigh user impact or an important regression.
    on Sep 23, 2026
  4. github-actions commented on Sep 23, 2026

    @github-actions
    Contributor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority:mediumA normal bug or meaningful improvement.triage:maintainableFits the repository's maintainable scope and needs review.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions