Background tasks mass-killed by memory-pressure reaper on Linux hosts with low MemFree but high MemAvailable (PSI ~0)
Summary
On a Linux server, Claude Code's background-shell memory-pressure reaper repeatedly kills all of a session's run_in_background Bash tasks simultaneously, even though the machine has ~39 GB actually available (MemAvailable) and kernel memory PSI is ~0. The box merely has chronically low MemFree (0.7–3 GB of 62 GB) because most RAM is page cache — normal for a busy server. The reaper's pressure signal appears to treat this steady state as memory pressure.
In one interactive session (v2.1.212) this fired 8 times over 4 hours, killing 14 background tasks — long-running subprocess dispatches and a Gradle test run — each event killing every live background task in the same second. Tasks die mid-stream with <status>killed</status> and the generic summary Background command "…" was stopped. Earlier sessions on the same host (07-14 → 07-16) show the same bursty signature (29 kill notifications total).
Environment
- Claude Code v2.1.212 (native installer), interactive CLI session
- Ubuntu, kernel 6.8.0-124-generic, x86_64, 12 CPU, 62 GB RAM
- Long-lived session dispatching multiple concurrent background tasks
Evidence
Kill events (UTC, 2026-07-17): 17:59:11, 18:04:21, 19:46:09, 20:23:22 (×2 tasks), 20:40:56 (×2), 21:30:12 (×3), 21:43:28 (×2), 21:47:14 (×2). Task lifetimes at death ranged 30 s – 10 min (no timeout param was set; all tasks run_in_background: true).
Everything else ruled out:
- No
TaskStoptool call anywhere in any session transcript; user did not stop tasks from the UI. - Kills happened while the session was idle (last turn ended minutes earlier).
- No session restart: single
SessionStarthook entry, single version, same sessionId throughout. - journald/dmesg clean at the exact kill seconds — no OOM killer, no systemd-oomd events, no cron/timer activity.
- Other concurrent Claude Code sessions' background tasks survived the same instants (consistent with a per-process event, not an external killer).
- Child processes die ~1 s before the kill notification is enqueued into the transcript; their logs end mid-stream with no error.
Mechanism (from the shipped binary): each backgrounded task registers a listener on the runtime's memoryPressure event; on the event it emits task_local_shell_pressure_reap, marks the task killed, and kills the tree — so one pressure event wipes every running background task at once:
if (i===void 0 && !un() && !Z.CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP) {
let a=()=>{ let l=r.get(e);
if (l?.status!=="running"||l.notified||Date.now()-a$()<SJg||Vri()||iCt(r.all())) return;
Ae("task_local_shell_pressure_reap"), dps(e,t,"killed",void 0,r,n,o,i), QOe(e,r) };
process.on("memoryPressure", a); ...
}
Memory state at kill times (sar, 10-min samples around the first two events): MemFree ≈ 0.7–3 GB, MemAvailable ≈ 38–40 GB, %memused ≈ 36 %, buff/cache ≈ 31 GB. /proc/pressure/memory: some avg10=0.00 avg60=0.00 avg300=0.02. No OOM events in the journal — ever.
Expected behavior
The pressure signal should not fire on a host where MemAvailable is high and PSI is ~0. On Linux, low MemFree alone is not memory pressure — page cache is reclaimable. Suggest keying the reaper on MemAvailable and/or /proc/pressure/memory instead of free-memory, and including the reap reason in the task notification (the generic was stopped made this very hard to diagnose — it's indistinguishable from a user/agent stop).
Workaround
CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP=1 (set via the env block in settings.json).
Related
Possibly related idle/background-termination reports: #75037, #72851, #72171.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗