Background tasks killed for "low memory" with 26 GB free on WSL2
What happened: Three consecutive background Bash tasks were terminated by the harness with «was stopped because the system is running low on memory», while free -m reported ~26,800 MB free and ~29,200 MB available out of 31,893 MB total. No kernel OOM events in dmesg. The killed processes were using ~200-500 MB each (Radiance/Python computations). Re-run identically in the foreground, they complete without problems.
Repro: On WSL2 (kernel 6.6.114.1-microsoft-standard-WSL2), launch a Bash tool command with run_in_background that runs Python/Radiance computations lasting a few minutes. The kill arrived within 1-2 minutes in 3 out of 6 cases (the other 3, identical, completed). dmesg shows «WSL (368) ERROR: No buffer space available @telemetry.cpp:190 (StartTelemetryAgent)» and periodic drop_caches from mini_init — the low-memory detection appears to misread WSL2's memory state.
Evidence:
- task ids:
bnwwtnmxg(completed),by5wat1od(killed),bdtksvw8h(killed),bkdarac7y(killed) - session:
session_01SexjAPCvf78ocpu6yETwet, 2026-09-06 ~01:00-01:30 CET - request ids:
req_011Cem9M3TtjkWAcSMejNtQf,req_011Cem9NZYRHKbom7newchci,req_011Cem9UGAZjS9kdsdnz1JVv free -mright after the kills: 26-27 GB free- CLI 2.1.261, linux x64, WSL2, model claude-fable-5
3 Comments
Third independent data point, same platform. Adding it because I was able to rule out three plausible causes by measurement, which narrows the search.
Environment
6.6.87.2-microsoft-standard-WSL2, 62.8 GiB allocated to the VMWhat was killed
Two background Bash tasks, both trivial
until <check>; do sleep 30; donewaiters holding essentially no memory. Both reported:with task status
killed. No user-visible work was lost, but the loops were the wait mechanism for a long-running build, so the session lost its completion signal.Ruled out by measurement
MemoryHigh=28G,MemoryMax=32G,MemorySwapMax=0. After the kills:``
``memory.current 3.3 GiB
memory.peak 12.1 GiB
memory.events low 0 high 0 max 0 oom 0 oom_kill 0
The scope never reached even the throttle threshold, and the kernel never killed anything in it.
oom-kill/Killed processentries indmesg.os.freemem()— would not have fired. On this kernel it returnsMemAvailable, which read 45.9 GiB. So a check based onos.freemem()cannot explain the kill.Memory state at inspection
Where this differs from #92228
That report hypothesises the guard reads
MemFreewhere it should readMemAvailable. That does not fit here:MemFreewas 12 GiB, which is unlikely to read as "low" under any plausible absolute or percentage threshold.The one number that was genuinely near exhaustion on this machine is swap headroom (295 MiB of 4 GiB). That makes me suspect the heuristic factors in free swap, or some combined free-memory-plus-free-swap figure, rather than
MemFreealone.Weak supporting signal
The swap pressure turned out to come from 55 leaked
testcontainers-labelled Postgres containers left running for 8 days; their shared memory had been swapped out. Removing them dropped swap usage from 3800 MiB to 878 MiB. I have not seen a kill since, but that is one observation over a short window, not a controlled result — I am reporting it as a lead, not as a reproduction.What would settle it
If someone can say which field the check reads (
MemFree,MemAvailable, swap headroom, PSI, or a combination), these three reports can be checked against it directly. A documented threshold, or any way to configure or disable it, would also help: I could not find one insettings-reference.mdorerrors.md.hitting multiple similar issues on my VPS, a lot of RAM (48GB total), plenty free/avail, yet getting those out of the blue which break a lot of flows
I wonder what it measures as "low memory", could it be tied to high load average numbers?
Started to happen ~ 1 week ago +- couple of days
Root cause found: this is a one-shot false memory-pressure event from a Linux kernel PSI bug, and Bun re-arms it each time a session starts a background shell. It has nothing to do with MemFree or MemAvailable.
The chain (each link read from source)
process.on("memoryPressure"). On the event it kills the task with reasonmemory_pressure, unless one of these holds:userPresence.lastInteractionTime);local_agent,remote_agent,in_process_teammateorlocal_workflowtask is live.The off switch is the documented
CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP=1.process.on("memoryPressure"), oven-sh/bun#32594, Bun 1.4). On Linux it writes the PSI triggersome 150000 2000000to/proc/pressure/memory, falling back to the cgroup'smemory.pressure, and emits onEPOLLPRI. The watcher is armed on the first listener and disarmed on the last removal. So every time a session goes from zero background shells to one, a new trigger is created.kernel/sched/psi.c, torvalds/linux master as of 2026-09-15).psi_trigger_create()seeds every trigger's window with thePSI_POLLtotal:``
c
``window_reset(&t->win, sched_clock(), group->total[PSI_POLL][t->state], 0);
...
t->aggregator = privileged ? PSI_POLL : PSI_AVGS;
CAP_SYS_RESOURCE) is evaluated inupdate_triggers(group, now, PSI_AVGS)aswindow_update(&t->win, now, total[PSI_AVGS][state]), i.e. thePSI_AVGStotal minus aPSI_POLLstart value.total[PSI_POLL]only advances while a privileged (rtpoll) trigger exists.prev_growthfor one window).Measured. Environment: WSL2, kernel
6.18.33.2-microsoft-standard-WSL2, a 94 GiB VM, CC 2.1.272.somegrowth around those events was 115 µs, 0 µs and 30 µs, with MemFree at 79 GB and no direct reclaim, kswapd scan or compaction.Controlled cycles. The same trigger was created and closed three times. Each fired at its first stall after arming (0.1 s, 60.1 s and 2.5 s after arming) with 687 µs, 520 µs and 0 µs of real
somegrowth. One of the three fired a second time within its next window, on 562 µs of growth; the other two stayed silent for 240 s. In all, six of six newly created triggers fired falsely.Why it looks random. A kill needs the false event to land after 30 minutes of user idleness, with the turn ended and no subagent running. The event waits for the first stall after the trigger was armed. On a quiet machine that can be minutes later, often when the background job itself starts heavy I/O. That matches the reports here: high MemAvailable, low MemFree, WSL2/Debian/Ubuntu, and even trivial
sleepwaiters being killed.Workaround. Set
"CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP": "1"insettings.json→env.Suggested fixes
/proc/pressure/memoryand require thesometotal to have grown by at least the threshold within the last window. Alternatively, keep one listener for the session's lifetime, so the trigger is not re-armed for every background shell.some totalgrew by less than the threshold since arming or since the previous event (reported as oven-sh/bun#42783).PSI_AVGSfor unprivileged triggers).