[BUG] Background workers (bg-spare) never release memory after jobs complete — monotonic RSS growth saturates 24GB RAM in ~7 days on always-on machines

Status Open
Reported on v2.1.234
Maintainer reply None cached
Activity 1 comment · opened Aug 20, 2026

Environment

  • Claude Code: 2.1.234
  • OS: macOS 26.4 (25E246)
  • Hardware: Mac Mini, Apple M4, 24 GB RAM
  • Node: v25.8.1 (Homebrew install of @anthropic-ai/claude-code)
  • Usage pattern: headless always-on dev machine; 7–11 concurrent claude agents --cwd <project> FleetView sessions kept alive 24/7 in tmux, plus the claude.exe daemon run spare pool

Summary

Long-lived background worker processes (claude bg-pty-hostclaude bg-spare pairs claimed from the daemon spare pool) show monotonic RSS growth that is never released, even when all their jobs are done and they sit fully idle. Individual workers grow from ~15–55 MB at spawn to 500–950 MB, and on an always-on machine this reliably saturates 24 GB of RAM in ~7 days, ending in a system-wide stall (SSH banner timeouts, fork() failures — every spawn returns ETIMEDOUT).

This appears distinct from the interactive-session leaks already reported (#33441, #67433, #32892): those show GB/min growth in a foreground TUI session, while this one is a slow burn (~0.44 GB/h aggregate) in detached background workers, so it only manifests on machines where sessions are never restarted.

Quantitative data (2-hour sampling, 3-min interval)

1) Largest single worker RSS is strictly monotonic — never drops once:

| time | rss_max (single bg-spare) |
|---|---|
| 07:19 | 703 MB |
| 07:28 | 802 MB |
| 07:43 | 847 MB |
| 07:58 | 851 MB (plateau ~25 min) |
| 08:22 | 895 MB |
| 08:32 | 937 MB |

+202 MB in 2 h, zero drawdown. Plateaus of 20–25 min occur, then growth resumes (event-driven, not time-driven).

2) Not explained by process count — total grows while count is pinned:

Process count stayed at exactly 28 for a full hour, yet the summed RSS of those same 28 processes grew 10.37 GB → 10.81 GB (+0.44 GB/h with zero new processes).

3) The spare pool itself is NOT the problem — it recycles correctly:

Spare count went 8 → 15 over ~55 min, then the daemon reclaimed back to 8 and held there for 1 h+. Pool hygiene is fine; the leak is per-process RSS in long-lived claimed workers.

4) Workers hold memory after their jobs finish:

Cross-referencing each claimed spare's cwd (lsof -d cwd) against ~/.claude/jobs/*/state.json: workers holding only done jobs kept 418–936 MB each indefinitely. In one cleanup, 9 idle worker pairs (all jobs done) held 7.8 GB; killing them released it fully and the daemon respawned a fresh 15 MB spare.

5) End state after ~7 days uptime (measured before reboot):

  • wired 23.4 GB / 24 GB, free 2%, memory pressure level 2
  • swap 11.2 GB / 12 GB used
  • 489 million cumulative swapouts ≈ ~7.8 TB written to the SSD (real hardware wear)
  • sshd took 9.8 s to fork (banner-exchange delay); cron jobs failed with ETIMEDOUT

Repro sketch

  1. On a 16–24 GB Mac, start several claude agents --cwd <project> sessions in tmux and leave them attached 24/7.
  2. Run a few background jobs in each so workers get claimed from the spare pool.
  3. Sample ps -Ao rss,comm | grep claude hourly: per-worker RSS climbs monotonically and never returns after jobs complete; extrapolates to full RAM in days.

Workaround we use

When all of a project's jobs are done: SIGTERM its claimed bg-spare + parent bg-pty-host pairs. Job records, transcripts (jsonl), and the FleetView dashboard are unaffected (state is on disk); the daemon replenishes the pool. Releases 100% of the held memory — which also suggests the retained allocations serve no purpose after job completion.

Happy to provide the full sampling logs or run instrumented builds.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗