Scheduled-task runs leak a resident claude process per fire (2.1.255, Linux): 106 procs / 26 GB in 25 h — regression of #54626

Status Open
Reported on v2.1.119
Maintainer reply None cached
Activity 1 comment · opened Sep 3, 2026

Environment

  • Claude Code: 2.1.255 (bundled in Claude Desktop, Linux)
  • OS: Arch Linux (Omarchy), kernel 7.1.9, Wayland/Hyprland
  • RAM: 30 GiB + 30 GiB zram + 30 GiB disk swapfile
  • Entrypoint: claude-desktop, built-in scheduled tasks (~17 registered, cadences from 30 min to daily)

Summary

Every scheduled-task fire spawns a resident claude CLI process that never exits after the run completes. Over 25.5 h of uptime this accumulated 106 leaked processes holding ~11.4 GB, and with per-session MCP servers layered on top the app's total footprint reached ~26 GB of 30 GB, saturating zram and pushing ~20 GB onto the disk swapfile.

This is the same failure described in #54626, which was closed as COMPLETED on 2026-06-16 at ~2.1.119–2.1.149. It reproduces on 2.1.255, on Linux, at roughly 70× the reported magnitude (that report cited ~1.5 GB over a week; this is ~11 GB in a day). Filing fresh rather than commenting on a long-closed ticket, but this should probably be treated as a regression of it. Related but distinct: #83218 (MCP subprocesses surviving app quit, macOS), #85986 (VSCode extension spawning a process per discussion).

Evidence that it's the scheduled tasks, not user-opened chats

Three independent lines, all from one host:

  1. Process census. 144 resident claude processes under a single desktop app instance (uptime 25.4 h). Classifying each by the first user message in its transcript: 106 were scheduled-task runs, 38 were real interactive chats.
  1. Start times are cron-shaped, not human-shaped. Session start minute-of-hour clusters hard — :04 ×17, :05 ×17, :34 ×15, :49 ×14, :19 ×12 — across only 36 distinct minutes of 60. Sessions also appear steadily overnight while the machine is unattended (00:13, 01:18, 02:05, 05:07, 06:01, 06:36).
  1. The transcripts say so directly. Every one of those overnight sessions opens with <scheduled-task name="…" file="~/.claude/scheduled-tasks/…/SKILL.md">, and each corresponds to a registered task's configured fire time.

Leak concentrated in the high-frequency tasks, exactly proportional to fire count:

| task (cadence) | leaked resident procs |
|---|---|
| poller-a (~30 min) | 50 |
| poller-b (~90 min) | 23 |
| mail-sync (hourly) | 13 |
| 14 others (daily) | 20 |

Measured rate: ~4.2 leaked processes/hour, ~110 MB each.

Session registry corroborates: ~/.claude/sessions/ held 143 entries, only 1 with a dead PID — i.e. essentially every task run since app start was still resident. All carried "kind":"interactive", "entrypoint":"claude-desktop", "nameSource":"derived".

Compounding factor: one MCP server per session, spawned whether used or not

Each leaked session also holds its full MCP server set. With the Playwright plugin enabled globally, that was 284 extra processes (1 npm wrapper + 1 node server per session) totalling 10.9 GB — for a tool the vast majority of those task runs never call. Disabling that one plugin cut per-session cost from ~185 MB to ~110 MB.

Two things would help independently of the leak fix:

  • don't spawn a plugin's MCP server until the session actually resolves one of its tools
  • ensure a session's MCP children die with it

A third, smaller contributor: /tmp/claude-<uid>/ scratchpad directories are not cleaned up per session and are RAM-backed on tmpfs (306 dirs / 3.7 GB here).

Impact

free -h at the point the desktop became unusable:

               total        used        free   available
Mem:            30Gi        29Gi       612Mi       818Mi
Swap:           60Gi        50Gi
/swap/swapfile  30.4G  19.9G used
/dev/zram0      30.4G  30.3G used   (100% full)

Memory PSI full avg300 = 1.35, IO PSI full avg300 = 3.69, CPU PSI full = 0.00. That last point is a diagnostic trap: the machine presents as generally sluggish with no CPU load at all, so anything that looks at CPU first finds nothing.

Reproduction

  1. Claude Desktop on Linux, 2.1.255, with several scheduled tasks registered (at least one at 30-min cadence).
  2. Leave the app running ~24 h.
  3. ps -eo pid,rss,args -C claude | grep claude-code | wc -l — expect roughly one process per task fire since app start, none of them exiting.
  4. Confirm classification: for each PID in ~/.claude/sessions/*.json, read the first type:"user" record of ~/.claude/projects/*/<sessionId>.jsonl; leaked ones begin with <scheduled-task name=.

Restarting the desktop app reaps them; a reboot resets the clock. Neither is a fix — accumulation restarts immediately.

Workaround

A local reaper that SIGTERMs finished task-run processes. It only targets sessions whose transcript opens with <scheduled-task name=, requires the transcript idle ≥60 min and ending on a completed assistant turn, and re-reads /proc/<pid>/cmdline before killing to guard against PID reuse. Validated by replaying 919 transcripts: 145 of 147 leaked runs correctly identified, 2 genuinely-interrupted runs spared, all 772 interactive chats untouched. Runs on a 4-hour user timer.

One detail that may be useful to whoever fixes this: transcript .jsonl files do not end with the final conversational record. They end with bookkeeping entries — last-prompt, bridge-session, atis-latch, queue-operation. A naive "is the last record a completed assistant turn?" check judged only 21 of 147 finished runs complete; skipping non-conversational types and reading the last user/assistant/result record raised that to 145. If any internal lifecycle logic decides "is this run done?" by inspecting the tail of the transcript, that may be the bug.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗