[BUG] Workflow tool: runner subprocess leaks memory unbounded (~70 MB/s) → OOM-killed on long multi-agent runs
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?
Running a long, multi-agent "dynamic Workflow" (the Workflow tool) in WSL2 intermittently leaks memory until the process is OOM-killed and the terminal session dies with "[process exited with code 1 (0x00000001)]".
The leak is NOT in the interactive TUI. During Workflow execution there are two claude processes: the main TUI, and a SEPARATE child claude process that runs the workflow (its process title is set to the version string, so it appears in ps/OOM logs as e.g. 2.1.179). On an affected run, that runner subprocess's anonymous memory climbs steadily at ~70 MB/s with no upper bound until it consumes the entire WSL memory allocation (~32 GB) and is SIGKILLed by the Linux OOM-killer. The TUI stays healthy the whole time (~300–500 MB).
The most important clue: the growth is completely decoupled from the actual workload. A run that OOM'd at 31.9 GB and a run that completed cleanly were near-identical in every recorded metric:
| Metric | OOM run | Clean run |
| --- | --- | --- |
| subagents | 13 | 14 |
| Bash tool calls | 127 | 124 |
| transcript bytes | 3.19 MB | 3.29 MB |
| total tool output | 2.23 MB | 2.22 MB |
| peak runner RSS | 31.9 GB | < 0.6 GB |
So ~32 GB of anonymous heap is wildly disproportionate to the ~3 MB of data the run actually handled — it's an unbounded internal allocation, not retention of legitimate data. It is intermittent (most runs are fine), which points to a timing/nondeterministic trigger rather than any specific input.
What Should Happen?
The Workflow runner subprocess's memory should stay bounded and roughly proportional to the data it processes. A ~14-agent run streaming ~1.5M subagent tokens (~3 MB of transcripts) over ~15 minutes should use well under 1 GB — as the healthy runs do (peak < 600 MB). It should never be able to grow unbounded to tens of GB and get OOM-killed.
Error Messages/Logs
Linux OOM-killer (journalctl -k / dmesg) at the moment of the crash:
systemd invoked oom-killer: gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE|__GFP_COMP), order=0, oom_score_adj=0
oom-kill:constraint=CONSTRAINT_NONE,...,global_oom,task_memcg=/init.scope,task=2.1.179,pid=3959,uid=1000
Out of memory: Killed process 3959 (2.1.179) total-vm:33809980kB, anon-rss:31943424kB, file-rss:296kB, shmem-rss:0kB, UID:1000 pgtables:62908kB oom_score_adj:0
oom_reaper: reaped process 3959 (2.1.179), now anon-rss:0kB
OOM "Tasks state" table — note TWO claude processes; the TUI is fine, the workflow-runner child is the hog (RSS in 4 KB pages):
[ 1168] ... 78061 pages ( ~305 MB ) claude <- interactive TUI, healthy
[ 3959] ... 7985930 pages (~31.9 GB) 2.1.179 <- workflow runner subprocess (anon-rss 31,943,424 kB), OOM victim
Separate minor bug found while reproducing: passing the Workflow `args` as a bare (non-JSON) string makes the workflow runtime throw immediately —
Error: JSON Parse error: Unexpected identifier "do" at workflow.js:9:49
(harmless: fails in ~8 ms with 0 agents — but worth hardening the args parsing.)
Steps to Reproduce
NOTE: intermittent — reproduces on a minority of runs. I caught it roughly 1-in-several; two controlled re-runs under a memory watchdog (including the exact same input as a crashing run) both stayed clean. So it's nondeterministic/timing-based, not input-driven.
- In WSL2 (Linux), session model = Opus 4.8 (
claude-opus-4-8[1m]). - Run a long, fan-out dynamic Workflow via the Workflow tool: ~13–14 subagents (a parallel/
parallel()discover phase of ~10 agents plus a couple of survey agents and a synthesis agent), each doing several Bash/Read tool round-trips, ~1.5M subagent tokens total over ~15 minutes. (Mine was a custom "field-note-scan" workflow: refresh a local dir, fan out ~10 parallel "miner" agents over text files + git history, then one synthesis agent.) - Externally watch the separate child
claudeprocess (process title = version string) viaps//proc/<pid>/status, or just sampleMemAvailablein/proc/meminfo. - On an affected run, that runner subprocess's anonymous RSS climbs steadily (~70 MB/s) unrelated to the data, until it hits the WSL memory cap and is OOM-killed. Most runs instead stay flat (< 600 MB) and complete normally.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
(N/A — intermittent; seen across 2.1.170 and 2.1.179.)
Claude Code Version
2.1.179 (Claude Code) — also observed on 2.1.170
Platform
Claude subscription
Operating System
Windows
Terminal/Shell
WSL (Windows Subsystem for Linux)
Additional Information
ENVIRONMENT
- WSL2 (Ubuntu) on Windows 11; kernel 6.18.33.1-microsoft-standard-WSL2, x86_64; host has 64 GB RAM.
- .wslconfig: memory=32GB, processors=8, swap=0 (also reproduced/observed with swap tried at 8/16 GB).
- The 32 GB WSL cap is why the OOM-kill happens inside the VM rather than wedging Windows.
KEY FINDINGS (from forensic analysis of the crash + 2 controlled re-runs)
- The leak is in a SEPARATE child
claudeprocess that executes the Workflow, NOT the interactive TUI (which stayed ~305–500 MB throughout). Only the runner subprocess balloons. - Growth is steady/linear at ~70 MB/s and DECOUPLED from the workload (see the table above. "What's Wrong?"). 31.9 GB of anon heap vs ~3 MB of actual data handled.
- Intermittent: could not force it on demand — 2 faithful re-runs (one with byte-identical input to a crashing run) both stayed flat (MemAvailable never dropped below ~30.7 GB).
RULED OUT (with evidence, so you don't chase these)
- Model / model-switching: pure Opus 4.8[1m], no Fable, no auto-mode swap.
- LSP servers: none were running (workspace had nothing the enabled LSPs index).
- WSL /mnt/c 9p page cache: the OOM was anon-rss (process heap), not file cache.
- Large input data: tiny (64 KB primary file; ~2.1 MB total corpus; subagent transcripts ≤ 552 KB).
- Project hooks / MCP servers: none configured.
- The bare-string args JSON.parse error: inert (0 agents, ~8 ms) — not the cause.
SECONDARY OBSERVATIONS
- With swap=0, as it approaches the cap the kernel enters a reclaim livelock (pinned CPU + heavy SSD I/O) for a while before the kill; a small swap smooths this but doesn't stop the leak.
- While any background Bash task runs, Claude Code rapidly spawns pairs of short-lived (~7 MB)
claudehelper subprocesses — harmless/transient, but possibly wasteful.
WORKAROUND
- Running the same fan-out work via the Task/Agent tool (in-process subagents) instead of the Workflow tool stays flat (~500 MB). The unbounded growth appears specific to the Workflow tool's runner-subprocess execution path.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗