[BUG] Headless CLI reserves memory from host RAM, leaks to 11-31 GB, and takes down the entire WSL2 VM (2.1.181 / 2.1.215 / 2.1.220)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Claude Code CLI grows to 11??1 GB anon-RSS and takes down the entire WSL2 VM
Version: 2.1.220 (also reproduced on 2.1.215 and 2.1.181)
Platform: WSL2 (WSL 2.7.8.0, kernel 6.18.33.1-microsoft-standard-WSL2), Ubuntu 24.04.1 LTS
Host: Windows 11, 63.4 GiB RAM; WSL2 VM allotted 31 GiB (default 50%) + 8 GiB swap
Install: ~/.local/bin/claude
Severity: Data-loss / availability ??kills every unrelated process in the VM, not just Claude Code
---
Summary
A claude process grows without bound until the kernel's global OOM killer fires. On WSL2 this does not merely kill Claude Code ??it takes down the whole Linux userspace, because every WSL process lives under init.scope and systemd ships init.scope with OOMPolicy=stop. One runaway claude therefore causes systemd to SIGKILL the entire scope: long-running servers, cron jobs, and every interactive shell.
For us this meant our production trading-research server and its ~24 scheduled jobs went down four separate times, twice in the last two days, with no operator action and no warning.
This is not a WSL misconfiguration. The amplification is WSL-specific, but the trigger is Claude Code allocating 11??1 GB of anonymous memory, and it has done so across three different releases spanning six weeks.
Evidence
Every OOM victim on this machine since the box was built is a Claude Code process. The process comm is Claude Code's own version string, which makes attribution unambiguous:
$ journalctl | grep 'Out of memory: Killed process'
Jun 18 23:48 Killed 7182 (2.1.181) total-vm:42181228kB anon-rss:30897600kB
Jun 18 23:50 Killed 7182 (2.1.181) total-vm:42181228kB anon-rss:30897600kB
Jun 18 23:52 Killed 7182 (2.1.181) total-vm:42181228kB anon-rss:30897600kB
Jul 23 21:06 Killed 19899 (2.1.215) total-vm:42220944kB anon-rss:30617664kB
Aug 01 02:23 Killed 26218 (2.1.220) total-vm:21256856kB anon-rss:14889024kB
Aug 02 04:54 Killed 6194 (2.1.220) total-vm:21256664kB anon-rss:11386560kB
$ claude --version
2.1.220 (Claude Code)
That is 30.9 GB and 30.6 GB of anonymous RSS on 2.1.181/2.1.215, and 11??5 GB on 2.1.220.
The footprint is not organic growth ??it is sized from host RAM
Note the total-vm figures: 42181228 vs 42220944 kB, and 21256856 vs 21256664 kB. Incidents weeks apart, in different sessions, land within 0.1% of the same virtual-memory size. That is not growth driven by a workload.
Cross-referencing #82188 (independent reporter, different machine, same 2.1.220) makes the pattern concrete:
| Report | Version | VM memory ceiling | observed total-vm | ratio |
|---|---|---|---|---|
| #82188 (J0E1464) | 2.1.220 | ~16 GB | 17,062,176 kB (16.3 GB) | ~1.02횞 |
| this report | 2.1.220 | 31 GB | 21,256,856 kB (20.3 GB) | ~0.65횞 |
| this report | 2.1.215 | 31 GB | 42,220,944 kB (40.3 GB) | ~1.30횞 |
| this report | 2.1.181 | 31 GB | 42,181,228 kB (40.2 GB) | ~1.30횞 |
Two things follow:
- The reserved size tracks available RAM, not the task. A bigger machine does not make this safer ??it makes the leak bigger. This is the signature of heap sizing derived from system memory (V8's default
max-old-space-sizeheuristic, or an internal "fraction of RAM" budget) rather than a bounded working set. - The ceiling changed between releases on identical hardware ??40.2 GB on 2.1.181/2.1.215 dropped to 20.3 GB on 2.1.220, same box, same 31 GB allotment. Something in the sizing path is version-dependent, which should make the allocation site straightforward to locate.
Because the value is near-deterministic per (version, host RAM), this looks reproducible rather than a rare race.
The kill cascade
Aug 02 04:54:26 kernel: snapfuse invoked oom-killer: ... global_oom, task_memcg=/init.scope
Aug 02 04:54:26 kernel: Out of memory: Killed process 6194 (2.1.220) anon-rss:11386560kB
Aug 02 04:55:56 systemd: init.scope: Killing process 659 (Relay) with signal SIGKILL
Aug 02 04:55:56 systemd: init.scope: Killing process 660 (bash) with signal SIGKILL
Aug 02 04:55:56 systemd: init.scope: Killing process 166039 (claude) with signal SIGKILL
Aug 02 04:55:56 systemd: init.scope: Failed with result 'oom-kill'.
Aug 02 04:55:56 systemd: init.scope: Consumed 28min 5.751s CPU time,
29.8G memory peak, 7.9G memory swap peak.
29.8G memory peak, 7.9G swap peak ??Claude Code exhausted 31 GiB of RAM and all 8 GiB of swap in a 28-minute session.
Confirmed on this box:
$ systemctl show init.scope -p OOMPolicy
OOMPolicy=stop
Boot records show the resulting unclean VM terminations (no shutdown record between boots):
$ journalctl --list-boots | tail -3
-2 ... Fri 2026-07-24 07:20 EDT ??Sat 2026-08-01 02:25 EDT
-1 ... Sat 2026-08-01 04:06 EDT ??Sun 2026-08-02 04:56 EDT
0 ... Sun 2026-08-02 05:03 EDT ??(current)
Context
- Invocation is headless/non-interactive:
claude --print "/<skill>" --dangerously-skip-permissions, run from cron, per-attempt wall-clock timeout of 1200??800 s. - Sessions are long and tool-heavy (WebSearch/WebFetch, subagents, large file writes).
~/.claude/projectsis 982 MB; the largest single session transcript is 9.0 MB, with a 7.8 MB subagent transcript alongside it. If transcripts or subagent state are retained in-memory unbounded for the session's life, that is a plausible source.- A healthy foreground session on this box sits at ~490 MB RSS, so the failure mode is roughly 20??0횞 normal.
Expected behavior
- Claude Code should not allocate tens of gigabytes. If a session legitimately needs to grow, it should bound itself and fail its own request rather than the machine.
- Claude Code should ship a self-imposed memory ceiling (a documented
--max-old-space-sizedefault, or an internal high-water check that ends the turn cleanly). - At minimum this should be documented for long-running/headless deployments, with a supported way to cap it.
Actual behavior
Unbounded growth to 11??1 GB, global OOM, and ??on WSL2 specifically ??destruction of every unrelated process in the VM.
Impact
Anyone running Claude Code headless on WSL2 alongside other services is exposed to total loss of that environment, triggered by Claude Code alone. The blast radius is not scoped to the agent. On this machine it repeatedly killed a production webapp and its full cron fleet in the middle of the night.
Reproduction
Not deterministic on demand, but observed 6 times across 3 releases over 6 weeks under: headless --print invocations of long, tool-heavy, subagent-spawning sessions on WSL2 with a large default memory allotment. Frequency has increased (Jun 18 ??Jul 23 ??Aug 1 ??Aug 2).
Related issues
Filing separately because the invocation path here is the headless claude --print cron agent across three releases over six weeks, and because the host-RAM scaling correlation above only becomes visible when these reports are read together. Happy to have this merged into whichever is canonical.
- #82188 ??same
init.scopecascade on WSL2,2.1.220, background daemon. Closest match; the table above combines its numbers with ours. - #76185 ??headless
-psession leaks to 10??5 GB RSS, Linux,2.1.205. - #81343 ??single background subagent balloons to 9.5 GiB in ~100 s ??global kernel OOM,
2.1.220. - #70523 ??kernel OOM (not V8 heap) from parallel subagent fan-out,
platform:wsl. - #4953 ??process grows to 120+ GB and gets OOM-killed,
platform:linux. - #83204 ??VM-wide memory exhaustion on WSL2,
2.1.220.
The volume of open, independently-filed reports here is itself a signal: this is not an exotic configuration.
Questions
- Is there a supported memory ceiling for the CLI? Is
NODE_OPTIONS=--max-old-space-sizerespected by the bundled runtime, and is it safe? - Are session transcripts and subagent state held in memory for the session lifetime, and does
~/.claude/projectssize feed back into resident memory? - Why is
total-vmnear-identical across independent incidents (42.18/42.22 GB, then 21.257/21.257 GB)? That looks like a configured ceiling and may point straight at the allocation site.
Workaround we deployed
For anyone hitting this before it is fixed ??this contains the damage but does not fix the leak:
- Run every
claudeinvocation inside a memory-capped transient cgroup (no root needed):
``bash`
systemd-run --user --scope --quiet --collect \
-p MemoryMax=10G -p MemoryHigh=8G -p MemorySwapMax=2G \
-- claude --print "/skill" --dangerously-skip-permissions
claude` alone, which normal retry logic then handles.
This converts a machine-wide global OOM into a cgroup-local kill of
- Flip the WSL amplifier so one OOM stops killing everything else:
``bash``
sudo mkdir -p /etc/systemd/system/init.scope.d
printf '[Scope]\nOOMPolicy=continue\n' | sudo tee /etc/systemd/system/init.scope.d/10-oom.conf
sudo systemctl daemon-reload
- Cap the VM in
.wslconfig(memory=24GB,swap=16GB,autoMemoryReclaim=gradual) so a runaway cannot starve Windows itself.
What Should Happen?
Anyone running Claude Code headless on WSL2 alongside other services is exposed to total loss of that environment, triggered by Claude Code alone. The blast radius is not scoped to the agent. On this machine it repeatedly killed a production webapp and its full cron fleet in the middle of the night.
Error Messages/Logs
Steps to Reproduce
See the above
Claude Model
Opus
Is this a regression?
Yes, this worked in a previous version
Last Working Version
_No response_
Claude Code Version
2.1.220(Claude Code)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
WSL (Windows Subsystem for Linux)
Additional Information
_No response_
3 Comments
Third independent machine, and two details narrow this usefully: not WSL2, and not headless.
Environment: Claude Code 2.1.220 (native installer,
autoUpdates: false), Proxmox/KVM VM, Ubuntu, 31 GiB RAM, systemd. Long-lived interactive tmux session under its own systemd unit, not--print. Zero MCP servers configured, which rules out the previously-fixed MCP leak family (2.1.132, 2.1.139, 2.1.208, 2.1.217). The leak still happens.The
total-vmcorrelation reproduces. Our kernel log, three kills in five days:Against the two in this report. Same version, same 31 GiB host RAM, different machines, different virtualisation:
| source | total-vm (kB) |
|---|---|
| this report | 21,256,856 |
| this report | 21,256,664 |
| ours (Proxmox VM, interactive) | 21,256,720 |
Three independent machines within 136 kB, or 0.0006%. Our other two kills sit on a second plateau matching each other to 0.0008%. Two stable plateaus, each under 0.001%, suggests two distinct sizing paths, and whatever computes them is not reading the workload.
2.1.221 does not fix it (no memory fix in its changelog).
A measurement warning for anyone else reporting this. On 2.1.221 the seat is a process tree:
claude daemon run,bg-pty-host,bg-spare, forked sessions. Sampling the single launcher pid reports 499 MB here while the tree totals 2,111 MB across 9 processes, a 4x under-count. If you trend "claude RSS" from one pid you may watch a flat, healthy line while the daemon climbs. Walk the cgroup'scgroup.procsinstead. Since #82188 is specifically a daemon leak, some existing measurements may be understating it.Containment, and a gotcha inside it. Same cgroup workaround as above, but at unit level via a drop-in (
MemoryHigh=12G,MemoryMax=16G), which applies to a running unit withsystemctl daemon-reloadand no restart. Verify by reading/sys/fs/cgroup/system.slice/<unit>/memory.max, notsystemctl show.Do not alarm on
memory.current. It counts reclaimable page cache. Ours reads 11 GB whileanonis 1.5 GB andfileis 8.6 GB. We wired an early-warning onmemory.currentand it fired continuously against a completely healthy seat. Alarm on theanonline ofmemory.stat.24 h after capping,
memory.eventsreadshigh 436, max 0, oom_kill 0: the soft cap is reclaiming and nothing has died. Our kill cadence was 45 to 53 h, so that is one incomplete window, not a fix.A healthy interactive seat here sits at ~450-500 MB RSS, so the kills run roughly 40x normal, consistent with the ~490 MB baseline reported above.
Happy to supply full OOM task dumps, or to run an instrumented build if that would help locate the allocation site.
*Posted by Sarge Marcelino. Figures copied verbatim from the host's kernel log.
Independent confirmation on 2.1.219, from a host six times smaller than the ones reported here — 8 GB rather than ~46 GiB. The reason for posting is your unanswered question 3: what causes the near-identical
total-vmacross incidents? We measured that from the other end, without knowing this issue existed, and the numbers line up with your "heap sizing from system memory" hypothesis.Everything below is measured on the machine. A raw sample series is attached at the bottom so every figure can be checked, including the ones that weaken the case.
Environment
claude-desktop1.24012.11 (dpkg -s)All times UTC.
1.
total_vmtracks host RAM — a third data point, at a much smaller sizeAt the moment of the kill the kernel's process table listed eight processes named
claude. Page size 4096 bytes; converted from the kernel's page counts:Two things stand out against what is reported in this thread:
Seven independent sessions share
total_vmof exactly 5.74 GiB — that is 75% of this host's 7.6 GiB. Your incidents show ~40.2 GiB and ~20.3 GiB on a ~46 GiB guest. Whatever sets this figure, it is not workload: these seven sessions were doing very different things, had been alive for between 7 minutes and 6 hours, and carried transcripts from 1.5 MB to 11 MB. Their resident sets range from 110 to 304 MB. The reservation does not move.**The killed process had a different reservation: 4.27 GiB, not 5.74 GiB.** Same binary, same host, same moment — so it was started differently, not merely loaded more heavily. Its rss/total-vm ratio is 0.81, inside the 0.65–1.30 band in your table.
2. The killed process, and why our sampler never saw it
All three inside
app-com.anthropic.Claude-8371.scope, allconstraint=CONSTRAINT_NONEandglobal_oom.user.sliceanduser-1000.sliceboth reportMemoryMax=infinityandMemoryHigh=infinity, andsystemd-oomdis inactive, so these are host-wide kernel kills with no cap to hit. Only the first is free of intervention on our side; the two at 13:31Z fall inside the window in which we were shutting the app down.No Electron process was killed. Every Electron process here carries
comm=claude-desktop;claudeis the session binary.claude-desktopappears in the OOM process tables but never as a victim.Our sampler tracks only processes carrying
--output-format, i.e. interactive agent sessions, and it recorded seven at that moment — one-for-one the same seven the kernel listed as survivors. The eighth, the one that was killed, it never saw at all, in any of 80 rounds. That is consistent with the triggers named in this issue: a headless--printinvocation or a spawned subagent, neither of which carries that flag. We cannot confirm which; the process no longer exists.It was not ballooning at the moment of death. The kernel produced two dumps 57 seconds apart (12:57:58Z and 12:58:55Z) and the process grew from 3524 MiB to 3540 MiB in between — 16 MB. It was already large. When it got there, we cannot say.
One detail that may matter for the ordering: the OOM was invoked by
PID: 707958 Comm: claude, one of the ordinary sessions. A session asked for memory, could not get it, and the kernel then chose the largest process. The sessions were the trigger of the moment, not the size.The kernel's
oom_killcounter in/proc/vmstatstands at 36 for this boot. A poller recorded two rises: +11 at 12:58:59Z (entirely before any intervention of ours) and +6 at 13:31:44Z (inside our shutdown window).3. On your unanswered question 2 — is session state held unbounded?
Partly, and it is slow rather than dramatic. Every long-running session grows steadily and never gives the memory back. Six sessions, 4.4–6.2 hours each, sampled every 10 minutes:
"endpoint" is (last − first) / window, the estimator most sensitive to noise. "fitted" is a least-squares slope. The third column excludes each session's first hour, because a process that has just started is still filling caches and that is warm-up, not retention. Excluding warm-up, all six land between 5.5 and 7.9 MB/h. The argument is the consistency across six independent sessions, not the precision of any one fit.
Session 9294 is the control. It was already 4.5 hours old at its first sample (age 16,154 s), so it lies entirely outside any start-up phase — which is why its fitted and fitted-\>1h figures are identical. A session that cannot be warming up grows at the same rate as the five that could. It also ran on a different day from the other five, so the growth tracks session age rather than time of day.
And the transcript does not explain it. Across all 267 rows with a valid transcript size, the correlation between transcript bytes and memory is r = 0.235. Session 9294 carries a 24–25 MB transcript while sitting at 254–331 MB — the same band as sessions with 4–11 MB transcripts. So if something is retained for the lifetime of a session, it is not simply the transcript.
What weakens this, stated up front: consecutive samples of the same session differ by a median of 7 MB, p90 24 MB, p95 32 MB, with one outlier of 85 MB. At ~6–8 MB/h the trend is invisible over 40 minutes and only separates from the noise over hours — anyone sampling briefly will reasonably conclude the memory is flat. Sessions also dip below their own starting value (9294 did so six times), and a short-lived session went down: pid 1622871, 168 MB to 133 MB over 2.7 hours.
4. An independent check on our sampler
The kernel's table doubles as verification of the measurement above, from a source we did not build. Kernel
rss+swapat 12:58:55Z against our own sample two minutes earlier:Six of seven agree within 5 MB. The outlier is the youngest process in the table — 440 seconds old, while all others had run for hours — i.e. the same start-up phase excluded above; eleven minutes later it read 245 MB against the kernel's 241 MB.
Impact and what the mitigations buy
The practical failure mode here was not a crash message: the desktop app's main window disappeared entirely while the sessions kept working.
xwininfoshowed no main window left on the X server, only a 200×200 and a 10×10 helper. No Electron process was OOM-killed, so we cannot show why the renderer went; the timing fits an allocation failure during the same episode and we are not claiming more than that. Restarting the app returned the memory: used 3.9 GB → 1.7 GB, available 3.7 GB → 5.9 GB.Recycling sessions before they get heavy addresses section 3 and does nothing for the killed process. Using the attached series: samples from sessions younger than 15 minutes (n=45) have a median of 229 MB; older than 4 hours (n=72), 289 MB. So recycling recovers roughly 60 MB per session, about 360 MB across six — against a single process holding 3.46 GiB that the sampler cannot even see. We have adopted the
MemorySwapMaxhalf of the cgroup workaround posted in this thread and will addMemoryMax.What would help
total_vmdifferent from the 5.74 GiB the interactive sessions share? That single answer would identify the process class involved.<details>
<summary>Raw sample series — 359 rows, 80 rounds, tab-separated (click to expand)</summary>
Columns:
epoch(Unix seconds, UTC) ·pid·age_s(session age in seconds) ·mem_MB(private process memory, MB) ·transcript_bytes.transcript_bytesis -1 where the size could not be determined — 92 of the 359 rows; those rows still carry a validmem_MB. The sampler did not run continuously: there is a 12-hour overnight gap and one 81-minute gap. The final round (epoch 1786023590, 13:39Z) contains six brand-new pids at 170–194 MB with ages of 227–329 seconds — those are the sessions restarted after the incident, not a sudden drop, and they are absent from the six-session table above.</details>
Follow-up from the third machine (Proxmox/KVM, interactive, not WSL2 and not headless) that
reported here on 2026-08-04. We have not reproduced it in 20 days. Below is a 53-hour run with
per-5-minute RSS sampling, including the parts that weaken the claim.
What changed on our side
Nothing in workload. Same box, same long-lived interactive tmux session under its own systemd unit,
same heavy tool use, still zero MCP servers.
Versions since our report: 2.1.220 → 2.1.236 → 2.1.237 → 2.1.238 → 2.1.239 → 2.1.240.
The measurement
Last three OOM kills, all on 2.1.220, all before we started sampling:
Cadence was 45–53 hours.
Most recent long run, on 2.1.238:
Three things about that run are worth separating:
of this workload, memory was being returned, not accumulated. That is the opposite of the
signature we reported in August.
The process was not killed. We restarted it deliberately because the model context window was
nearly full — an unrelated reason, from the layer above. So 53h 29m is a floor on what it would have
survived, not a ceiling.
Zero OOM kills on this host since 2026-08-03.
The confound, stated plainly
On 2026-08-03 we also set
MemoryHigh=12GandMemoryMax=16Gon the unit. That is exactly the kindof change that can hide a leak rather than fix it, so here is the cgroup counter:
The hard cap never fired.
max 0andoom_kill 0mean the process was never once contained bythe ceiling we added — there was nothing to contain. The
highevents are page-cache reclaim, whichwe measured separately in August: at an apparent 11 GB the split was anon 1.5 GB / file 8.6 GB, of
which 7.3 GB inactive_file. A seat that reads and writes this many files accrues gigabytes of
droppable cache that can never cause an OOM. The caps are not what is keeping the number low.
The changelog entries that look related
That matches this issue's profile closely: long interactive sessions, retention rather than
allocation. 2.1.239 also carries
Fixed WebFetch retaining expired page content in memory for the, which is the same shape.whole session instead of the intended 15 minutes
Our 53h run above ran on 2.1.238, and that version is recorded per-sample from
/proc/<pid>/exerather than inferred. An earlier clean 45h run was on 2.1.236, i.e. before the 2.1.238 entry — so
we cannot attribute the improvement to that changelog line. Either something landed earlier than
2.1.238, or our improvement has a different cause. We cannot separate those from here.
What this is NOT evidence of
--printpath or the background daemon in #82188. Ours is interactive.Posting it because the opposite result — a machine that used to die every two days and now runs past
that window at 2% of the footprint — seems worth having in the thread alongside the reproductions,
and because the sampler lets us put numbers on "it stopped" instead of an impression.
Happy to share the sampler script or the raw TSV if either would help.