anthropics / anthropics/claude-code

Single background subagent (run_in_background, no tool use) balloons to 9.5 GiB in ~100 s → global kernel OOM (2.1.220, Linux, no swap)

Open
#81,343 5 comments 0 reactions 0 assignees View on GitHub
area:agents
Dominant language
Python
Stars
145k
Forks
23.1k
PR merge metrics
PR metrics pending

Description

## Summary

A **single, non-nested background subagent** (Task tool, `run_in_background: true`) spawned by an interactive CLI session grew from ~0 to **9.5 GiB anon-RSS in at most 103 seconds** and triggered a **global kernel OOM** on a 15.6 GiB Linux host. The interactive parent process stayed at ~452 MiB throughout and survived the kill.

Sustained allocation rate ≈ **95 MB/s**.

The subagent's prompt was a pure-reasoning task explicitly instructed to read no files and use no tools, so there was no large tool output, no file read, and no transcript resume involved.

## Environment

- Claude Code **2.1.220**, native installer (`~/.local/bin/claude` → `~/.local/share/claude/versions/2.1.220`)
- Ubuntu 24.04.4 LTS, kernel `6.8.0-117-generic`, x86_64
- 15.6 GiB RAM, **swap = 0** (`Total swap = 0kB` in the OOM dump)
- Interactive session running inside a tmux 3.4 pane. tmux places each pane in a transient systemd user scope (`tmux-spawn-.scope`), which is what makes the cgroup attribution below available.

## Timeline (relative to subagent launch)

| t | Event |
|---|---|
| t+0 s | Background subagent launched via Task tool (`run_in_background: true`, `subagent_type: general-purpose`, model opus). Pure-reasoning prompt, no file reads. |
| t+14 s | The **parent** session (not the subagent) began a foreground Bash `grep` over a 275 MB file. Ruled out below. |
| **t+103 s** | Kernel global OOM. Victim: a `2.1.220` process at 9.5 GiB anon-RSS. |
| t+103 s | The subagent's own output `.jsonl` stops writing at the same millisecond. |
| t+103.1 s | Parent process (comm `claude`, 452 MiB) still alive, writes 3 more records, then exits cleanly. |

## Evidence (verbatim kernel log; hostname, UUIDs and paths redacted)

```
kernel: 2.1.220 invoked oom-killer: gfp_mask=0x140dca(GFP_HIGHUSER_MOVABLE|__GFP_COMP|__GFP_ZERO), order=0, oom_score_adj=100
kernel: oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=user.slice,mems_allowed=0,global_oom,
task_memcg=/user.slice/user-1000.slice/user@1000.service/app.slice/tmux-spawn-.scope,task=2.1.220,pid=,uid=1000
kernel: Out of memory: Killed process (2.1.220) total-vm:10769744kB, anon-rss:9956352kB,
file-rss:2048kB, shmem-rss:0kB, UID:1000 pgtables:19816kB oom_score_adj:100
```

Relevant rows of the same OOM task table (`rss` column is in pages):

```
[ pid ] uid tgid total_vm rss rss_anon rss_file rss_shmem pgtables_bytes swapents oom_score_adj name
[] 1000 332836 1024 480 544 0 126976 0 100 2.1.220 <- launcher, 4 MiB
[] 1000 2692436 2489600 2489088 512 0 20291584 0 100 2.1.220 <- worker, 9.5 GiB
[] 1000 1503502 115726 114370 1356 0 2109440 0 100 claude <- interactive parent, 452 MiB (survived)
```

Note the `comm` values: child CLI processes appear as **`2.1.220`** because the native installer's binary file is literally named after the version, while the interactive parent launched through the `claude` symlink appears as `claude`. This is the same retitling signature reported in #76185.

## Why the concurrent `grep` is not the cause

The parent's foreground `grep -aoE '[^"]{0,70}timeout[^"]{0,70}'` over a 275 MB file is a plausible-looking suspect (bounded repeats + `-o`, cf. #76242 / #78861), so it was tested rather than assumed:

- Controlled replay of the **identical** command under `/usr/bin/time -v`: 3 min 03 s wall, **peak RSS 25.7 MB** — not 9.5 GiB.
- It runs as its **own** process (separate PID, ~23 MB RSS observed during replay), not inside the CLI process.
- The OOM victim's `comm` was `2.1.220`, i.e. the CLI binary, not `grep`/`ugrep`.

So the known grep-shim memory issues are **not** what happened here.

## Attribution confidence — stated honestly

The kernel OOM dump does not carry PPID, so identifying the victim as *the background subagent* is **inferential**, not proven. The converging signals:

- adjacent PID pair in the same cgroup: `` (4 MiB, launcher) / `` (9.5 GiB, worker), both comm `2.1.220`;
- the PID range corresponds to a spawn at t+0;
- `oom_score_adj=100`, matching the other CLI processes;
- the subagent's output `.jsonl` stops at the OOM millisecond;
- the interactive parent is separately present in the same table and survived.

I would rather flag this as high-confidence-inferential than assert it as established.

## Reproduction

**Not reproduced** — I want to be straight about this rather than invent a repro. This is a single occurrence (one `global_oom` in the host journal over the preceding week), and subsequent comparable sessions have not reproduced it. The contextual conditions were:

- interactive CLI session, ~65 min old at the time, running in a tmux pane on Linux;
- exactly **one** background subagent in flight (`run_in_background: true`, general-purpose, opus), no nesting, no fan-out;
- that subagent's task was pure reasoning — explicitly told not to read files or use tools — so it should have had a small, roughly constant working set;
- the parent was concurrently running one foreground Bash command;
- host had zero swap, so there was no reclaim runway to absorb a spike: the allocation went straight to global OOM.

Given the rate (~95 MB/s) and the absence of any large input on the subagent's side, this looks like unbounded allocation in the child CLI process itself rather than buffering of anything the task produced.

## Host-side amplification (not a CLI bug — context for the impact)

Worth recording because it turned a recoverable child-process kill into a lost session: systemd's default `OOMPolicy=stop` applies to the pane's transient scope, so when the kernel killed one process in that cgroup, systemd **stopped the whole scope** — SIGTERM to the cgroup, then SIGKILL of the pane's shell after `TimeoutStopSec=90s`, destroying the tmux session. The CLI process itself was an innocent bystander in that second step.

## Mitigation currently in place

Per-pane cgroup bounds on the transient scopes, so a leak dies inside its own cgroup instead of starving the host:

```ini
[Scope]
OOMPolicy=continue
MemoryHigh=5G
MemoryMax=6G
```

Verified end-to-end on a disposable pane: the runaway child is killed by the cgroup OOM killer, and the pane, its shell and the session all survive. 4 GiB of zram swap was also added to give the kernel reclaim runway. This is the same class of workaround as the one described in #76185.

## What would help from the product side

- A memory ceiling (and/or recycling) for background subagent child processes, so one child cannot exhaust the host.
- Memory-aware backpressure on subagent spawns, keyed on observed RSS rather than only CPU-count heuristics.
- If a child exceeds a ceiling, fail that subagent with a clear error rather than letting the kernel choose the victim — on a host without swap the kernel's choice can land anywhere.

## Related issues

- #76185 — same comm-retitle signature, Linux, kernel OOM, and the same cgroup workaround; but the trigger there is a long-running **background Bash task** with growth over 15–25 min, whereas this is a background **subagent** with no tool use growing in ~100 s.
- #74001 — multi-subagent load on Linux, subagent CLI processes ~6.5 GB RSS each (2.1.187). This report is a single, non-nested subagent.
- #74035 — deeply-nested subagent fan-out, 26 GB (2.1.200). No nesting here.
- #79196 — headless `--resume` transcript reification, ~140 MB/s. No resume involved here.
- #76242 / #78861 — grep-shim OOMs; explicitly ruled out above by controlled replay.

Contributor guide

No contributing guide indexed for this repository

Research direction

No source files or tests are identified. Start by comparing the behavior and cgroup workaround with related issues #76185 and #74001, then investigate how background subagent processes are spawned and monitored. Done means a runaway child is contained or fails clearly without taking down the host or parent session.

Written by the indexing model from the issue text.

Assessment

Tech stack
linux, python
Domain
cli, devtools, operating-systems
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.