Headless/SDK session supervisor never reaps child `claude` processes — unbounded memory growth to cgroup OOM
Environment
@anthropic-ai/claude-code: 2.1.220 (native binary, linux-arm64)- Node.js: v24.18.0
- OS: Ubuntu 26.04 LTS (aarch64)
- Consumption path: a long-lived, non-interactive
claudeprocess running in headless/server mode as a session supervisor — it accepts remote/SDK-driven sessions over its lifetime and spawns a childclaudeprocess per session. The supervisor itself is a persistent daemon; the children are meant to be per-session and short-lived.
Summary
The supervisor claude process never reaps its child claude processes after their session ends. Children accumulate for the entire uptime of the daemon (days), each retaining its full resident memory, until the daemon is restarted. Under a bounded cgroup this drives memory monotonically to the limit and produces repeated kernel OOM kills.
This is distinct from a caller aborting or a single session misbehaving: the children are idle — zero CPU-time growth and zero I/O across repeated multi-minute observation windows — yet they are retained indefinitely.
Evidence (measured)
On a single supervisor daemon, before intervention:
- 22 child
claudeprocesses had accumulated under one supervisor. - Combined resident memory ≈ 6.35 GB.
- The oldest children were 6+ days old — far outliving the sessions that created them (sessions that had long since completed).
- Sampled every 30 minutes over multiple windows, the children showed no
utime/stimegrowth (/proc/<pid>/stat) and norchar/wchargrowth (/proc/<pid>/io) — i.e. they were doing no work, just holding memory. - The daemon runs under a cgroup with
memory.max = 10 GB. As the children accumulated, the cgroup logged 13,000+memory.highthrottling events and the kernel issued 50 OOM kills in a single day, in two waves. - The only thing that clears the children is restarting the whole supervisor — which also tears down every live session it is currently hosting. There is no per-child cleanup path exposed.
Expected behavior
When a session ends (normally, on error, or on disconnect), the supervisor should reap that session's child claude process and release its memory. A child whose session has ended and which is doing no work should not survive to the next session, let alone for days.
Impact
For any deployment that runs claude as a persistent headless supervisor (multi-session servers, CI runners, remote-control style hosts), memory grows without bound over the daemon's uptime and eventually hits the host or cgroup limit, causing OOM kills that can take out unrelated processes on the box. The only mitigation available to operators is a scheduled full-daemon restart, which is disruptive because it drops all live sessions.
Related
- #68933 (closed
not_planned) reports the same class of defect from a different surface:skill-creator's eval harness shells out one headlessclaude -pper query, and the MCP servers those-pinvocations boot are never reaped. That report is plugin/-p-specific; this one is about a persistent supervisor never reaping its own session-childclaudeprocesses. The recurrence across two unrelated surfaces suggests child-process lifecycle in headless/SDK mode is under-managed generally, not a plugin-local bug.
Workaround deployed at our layer (offered for others hitting this)
Because the leak is inside the binary and cannot be patched, we added an external, conservative per-child reaper (systemd timer, 30-min cadence):
- Classifies a child as idle only after 6 consecutive quiet observations (~30 min apart) with no CPU-time and no I/O growth, matched by pid+starttime.
- Enforces an age floor (never touches a child younger than 30 min) and a per-run reap cap.
- Never touches the supervisor itself, and rechecks for live descendants immediately before acting (TOCTOU close) so a child that just became active is spared.
- Ships in observe-only mode first, then
SIGTERM→ grace period →SIGKILLon confirmed-idle children.
This contains the symptom but the underlying reap-on-session-end gap is in the CLI. A built-in reap of ended-session children would remove the need for any of this.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗