Deeply-nested subagent fan-out causes unbounded memory growth → host-level OOM

Open 💬 3 comments Opened Jul 3, 2026 by tolldog

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 (*.jsonl session 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 Read tool 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 claude process 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 deprioritized oom_score_adj, since the kernel kept hunting for memory across the whole cgroup/host.

Suggested mitigations

  1. Cap subagent spawn depth and/or total subagent count for the plain Agent tool (not just Workflow), or surface a configurable limit.
  2. Apply the same size-bounded read discipline inside subagents that the top-level Read tool already enforces, especially for large log/transcript-style files.
  3. 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.
  4. 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.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗