/resume picker reads the entire ~/.claude/projects into a ~23 GB heap at open (no session index since v2.1.30)

Open 💬 0 comments Opened Jun 13, 2026 by lazyckc

Version: 2.1.175 (native installer, Linux x86_64)
Severity: can OOM the machine; previously crashed a WSL2 install repeatedly, now survivable only behind a 96 GB swap file.

Summary

On a machine with a large session corpus, opening /resume (or claude --resume) spikes RSS to ~22.6 GB for ~2 minutes, then frees almost all of it once a session is picked. The picker appears to read and parse every transcript in ~/.claude/projects at open, with no persisted index, and reads each file multiple times.

Environment / scale

  • ~/.claude/projects: 514 project dirs, ~1.13 GB of .jsonl, 2,663 transcripts.
  • Largest single sessions: 134 MB (45k lines), 84 MB, 46 MB — all legitimate long multi-day sessions, no transcript-bloat duplication.
  • cleanupPeriodDays: 3650 (transcripts deliberately retained).

Measured behaviour (procfs sampling at 0.2 s, RSS + /proc/PID/io)

| t (s) | RSS | rchar (logical read) | physical read |
|------|-----|----------------------|---------------|
| 0 | 0.36 GB | 0 | 0 |
| 14 | 2.0 GB | 0.9 GB | 0 |
| 34 | 16 GB | 9.0 GB | 3 MB |
| 59 | 22.6 GB peak | 9.0 GB (flat) | 3 MB |
| 120 | 21.7 GB | 9.0 GB | 3 MB |
| 132 | 0.43 GB | — | — |

Two key observations:

  1. ~9.0 GB logically read to enumerate a 1.13 GB corpus → ~7–8× over-read. Each transcript is read several times to build picker metadata (first line / last prompt / stat / enrichment). Only 3 MB hit disk (page cache), so this is pure CPU + allocation, not I/O-bound.
  2. **RSS climbs another ~6 GB after all reads complete** (t+34→59) — ~25 s of JSON→object allocation. Peak heap ≈ 2.5× bytes read ≈ 20× the corpus size.

The memory is fully reclaimed on selection, so it is a transient working-set explosion, not a leak.

Likely cause

The v2.1.30 changelog replaced sessions-index.json with "lightweight stat-based loading and progressive enrichment." For small corpora this is a win; for large corpora the picker now re-derives all metadata from raw transcripts on every open, with no persisted index and with repeated reads per file. Related: #22459 (closed not-planned), #26123, #25032 (index staleness, open), #60573/#62397 (index/ledger requests).

Impact

  • 22.6 GB transient is fatal on typical-RAM machines; reporter had to move off WSL2 and provision a 96 GB swap file specifically for this.
  • Scales with corpus size, so heavy/long-term users are hit hardest while the median user barely notices — which likely keeps it under-reported.

Suggested fixes (in increasing order of effort)

  1. Persist a metadata index (id, cwd, mtime, size, first/last prompt) updated incrementally on session write; have the picker read the index, not the transcripts (this is what claude --resume <id> effectively needs anyway, and what the Codex CLI does via session_index.jsonl).
  2. Read each transcript at most once, and only the bytes needed (head + tail), during enrichment.
  3. Lazy/paged enrichment: enrich only the visible window of the list, on demand.
  4. A maxSessions / resumeScanLimit setting as an interim cap.

Workarounds confirmed locally

  • systemd-run --user --scope -p MemoryMax=32G claude to keep the spike from wedging the box.
  • Moving cold project dirs out of ~/.claude/projects reduces the peak proportionally, but only ~10% here because most bytes are in actively-resumed sessions.
  • Resume by id (claude --resume <id>) to bypass the picker.

Happy to share the raw 0.2 s procfs sample CSV.

View original on GitHub ↗