Deeply-nested subagent fan-out causes unbounded memory growth → host-level OOM
Deeply-nested subagent fan-out causes unbounded memory growth → host-level OOM
Note: This report was authored by Claude (running as Claude Code) via self-analysis of its own crash — investigating dmesg/OOM logs and its own session transcripts on the host after being OOM-killed, at the user's request. It is not a first-hand human account; treat the diagnosis below as Claude's own read of the evidence, not confirmed root-cause from the Claude Code engineering team.
Summary
A single claude CLI process grew from ~1.7GB to 26GB RSS over ~10 minutes while running a Workflow/Agent fan-out, and was eventually killed by the Linux OOM killer. The OOM killer's cascade also took down unrelated system services (ollama, tabbyapi, steamwebhelper), since they had lower oom_score_adj and were sacrificed first while hunting for enough memory to satisfy the allocation.
Environment
- Claude Code version:
2.1.200 - OS: Ubuntu (kernel
7.0.0-27-generic) - Host: 64GB RAM, 30GB zram swap (swap was NOT exhausted — ~26GB swap still free when the kill happened, so this was a fast allocation spike outpacing reclaim, not gradual pressure)
What triggered it
A self-directed "friction audit" task: scan the .claude/projects/**/*.jsonl session-transcript history across several project directories looking for pain points. This was implemented as a fan-out of subagents:
- 15 subagents spawned via the Agent tool
- Nesting up to spawnDepth 4 (agents spawning agents spawning agents spawning agents)
- Several branches targeted transcript files that were individually 20+MB (
*.jsonlsession logs) - The task was partially self-referential: subagents were reading Claude Code's own session history, including transcripts belonging to sibling/nested subagents in the same run
Evidence
dmesg samples across the run showed the same parent PID's anon-rss climbing continuously while its children (bash/head/wc/sleep) were short-lived and rotated — i.e. growth wasn't from one runaway command's buffered output, but from the orchestrating process itself accumulating memory across many tool calls/subagent completions without releasing it:
15:51:56 anon-rss: 1,719,110 kB (~1.7GB)
15:52:52 anon-rss: 2,801,688 kB (~2.8GB)
16:00:54 anon-rss: 6,099,861 kB (~6.1GB)
16:01:29 anon-rss: 6,585,962 kB (~6.6GB)
16:02:48 anon-rss: 26,572,044 kB (~26GB) → OOM-killed
Final kernel log line:
Out of memory: Killed process 1852048 (2.1.200) total-vm:50589092kB, anon-rss:26572044kB, ... oom_score_adj:0
Why this looks like a product gap, not user error
- The Agent tool has no documented spawn-depth or total-subagent-count cap (unlike the Workflow tool, which documents
min(16, cores-2)concurrency and a 1000-agent lifetime backstop). Recursive agent-spawns-agent chains can fan out multiplicatively with no built-in backpressure. - The top-level
Readtool defaults to a 2000-line cap and requires explicit override to read more — a deliberate memory guard. It's unclear whether subagents' own tool use inherits equivalent discipline when reading large files (some transcripts here were 20+MB). - Subagent transcripts/results appear to stay resident in the orchestrating process's memory rather than being streamed to disk and released once a branch completes.
- There's no visible memory-aware backpressure — concurrency is bounded by CPU heuristics, not by observed RSS, so nothing throttled the fan-out as the process approached the host's actual memory ceiling.
Impact
- The
claudeprocess itself was OOM-killed, losing the in-progress session state. - Collateral damage: the OOM killer also killed unrelated host services (
ollama,tabbyapi,steamwebhelper) that happened to have deprioritizedoom_score_adj, since the kernel kept hunting for memory across the whole cgroup/host.
Suggested mitigations
- Cap subagent spawn depth and/or total subagent count for the plain Agent tool (not just Workflow), or surface a configurable limit.
- Apply the same size-bounded read discipline inside subagents that the top-level
Readtool already enforces, especially for large log/transcript-style files. - Stream/flush subagent transcripts to disk and release them from parent-process memory once a branch completes, rather than retaining full history for the life of the run.
- Add memory-aware backpressure (watch RSS/cgroup memory, throttle new spawns) as a companion to the existing CPU-based concurrency caps.
Happy to provide the raw dmesg output / session transcript timestamps if useful for repro.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗