Scheduled tasks leak headless claude.exe --resume processes on Windows (never exit, accumulate until OOM)
Summary
On Windows, a recurring scheduled task (created via Claude Code's scheduled-tasks feature) spawns a headless claude.exe --resume <id> --output-format stream-json worker on every fire. The worker finishes its work but never exits — it sits idle indefinitely. Over hours these accumulate until the machine runs out of RAM and freezes (hard reboot required). Still reproduces on 2.1.170.
This looks related to #58565 (closed as duplicate) and #42169 / #54130 (both closed "not planned"), but the trigger here is different — a scheduled task on a timer, not closing a session window or opening the panel — and it still happens, so filing fresh with current details.
Environment
- Claude Code 2.1.170 (channel
latest; embedded in Claude Desktop, no standalone CLI) - Claude Desktop 1.12603.1.0
- Windows 11 Pro
- Several MCP servers configured (Slack, Atlassian, Bitbucket, Postman, …)
Repro
- Create a recurring scheduled task (~7-minute cron) that runs a trivial step and stops (e.g. a watcher task that just runs a script via Bash).
- Leave it running for several hours.
- Watch the
claude.exeprocess count and total memory climb.
Expected
Each scheduled run's headless process exits when the run completes.
Actual
Each run leaves a resident claude.exe --resume … --output-format stream-json --input-format stream-json --permission-prompt-tool stdio process:
- ~95 orphaned workers accumulated over ~10 hours (one per ~7-min fire).
- ~250–430 MB each; ~36 GB total across all
claude.exebefore the machine OOM'd and froze. - Each worker burned ~3–5 min of CPU (it did run the work) then sat at ~0% CPU forever — finished, then hung instead of exiting.
- All parented to the Claude Desktop supervisor process.
- Separately, 2,322
local_*.jsonsession files (~622 MB) piled up under%APPDATA%\Claude\claude-code-sessions\…over ~3 weeks, never pruned.
Likely cause
The headless --resume worker blocks waiting for a stdin EOF the launcher never sends (no idle timeout in headless mode), so it never terminates. The configured MCP servers loaded on each run also keep connections/handles open, inflating each worker's footprint.
Impact
System-wide memory exhaustion → laptop freezes → hard reboot. It's silent (no UI), so it's easy to miss until the machine locks up.
Workaround
Disable the scheduled task and reap the orphans:
Get-CimInstance Win32_Process -Filter "Name='claude.exe'" |
Where-Object { $_.CommandLine -like '*claude-code*--output-format stream*' -and ((Get-Date)-$_.CreationDate).TotalMinutes -gt 10 } |
Stop-Process -ForceShowing cached comments. Read the full discussion on GitHub ↗
7 Comments
Your root-cause reasoning lines up with how headless
--resumebehaves: with--input-format stream-json, the worker keeps stdin open waiting for more newline-delimited input and only tears down on EOF. A timer-launched worker that finishes its turn has nothing left to read but never receives the EOF (the launcher doesn't close the pipe), and there's no idle timeout in that mode — so it parks at ~0% CPU holding its heap (plus the MCP connections loaded that run). The per-run accumulation you measured is the expected outcome of that, not a separate leak.One thing worth flagging on your reaper before you leave it running: the
TotalMinutes -gt 10filter keys off process age, which also matches a worker that is legitimately still doing work on a long run. If any scheduled task ever takes >10 min, an age-only sweep willStop-Process -Forceit mid-flight — trading a memory leak for silent truncated runs. A safer signal is "finished, then parked": sample CPU twice and only reap workers whose CPU time stopped advancing while age is past your run's normal ceiling. Roughly (Windows; please verify on your box, I'm reasoning from the repro rather than a Win11 machine here):That only kills workers that burned no CPU across the sample window — an actively-working run is spared.
For the second pile you noticed — the 2,322
local_*.json(~622 MB) under%APPDATA%\Claude\claude-code-sessions\— there's no pruning, so it grows forever. Until that's fixed, a safe age-based sweep that keeps recent sessions:And since the leak is silent until OOM, the most reliable mitigation until there's an idle timeout in headless mode is to make the reap self-healing rather than manual: register the CPU-delta sweep above as its own small recurring task (every ~15 min). That way orphans never accumulate to the OOM point even if you forget, and it won't touch a run that's still working.
The real fix is upstream — an idle/EOF-less timeout on
--resume … --input-format stream-jsonworkers so a finished headless turn exits on its own, plus retention/pruning on the session-file directory. This is the same headless-worker-never-exits family as #42169 / #54130 / #58565, but you're right that the timer trigger is a distinct path and still reproduces on 2.1.170, so it's worth keeping open on its own.Note: the analysis below was written by Claude Code (Anthropic's assistant), running on the affected machine — I'm posting it on its behalf as the issue author. The diagnosis and command-line details are its findings, not mine.
Following up with empirical detail on the current behavior (still 2.1.170, Windows 11):
Confirmed the leaked workers do carry
--resume. After a ~7-minute recurring scheduled task had been running, the onlyclaude.exeprocesses on the machine with--resumewere the scheduled-task workers themselves:They park at ~0% CPU holding their heap (plus that run's MCP connections) and never exit — exactly the "stdin never gets EOF / no idle timeout" path @yurukusa described. Notably, the interactive session did not carry
--resume, so on this build--resumeactually marks the leaked workers, not the live session (worth knowing if anyone scripts a cleanup — keying off "no--resume" misses them).Workaround that sidesteps it entirely (for the "run a script on a timer" case): don't schedule it as a Claude Routine — let the OS scheduler run the script directly. On Windows, launching it via
pythonw.exefrom Task Scheduler spawns no headlessclaude.exeat all, so nothing leaks (and no console window flashes each run). That only applies when the scheduled job is a plain script that doesn't need Claude itself.Agreed the real fix is upstream: an idle/EOF-less timeout on
--resume … --input-format stream-jsonworkers so a finished headless turn exits on its own.Corroborating on Windows 11 with the bundled claude-code 2.1.181, so this still reproduces past 2.1.170.
Data point: recurring scheduled tasks (routines) accumulated 32 resident
...\claude-code\<ver>\claude.exeworkers over ~2 days, ~6.9 GB total. Start times map 1:1 to hourly-ish fire times (:00/:03/:07). Each finishes its work, drops to ~0% CPU, and never exits.Second-order impact: a finished-but-resident worker can keep holding the global
scheduled_tasks.lock. A task sharing that cron slot then loses the lock race and is silently rolled forward without running (lastRunAtstays stale), so the leak also makes scheduled runs skip, not just OOM.Safe workaround (daily scheduled PowerShell reap of prior-day workers only):
Filtering on
\claude-code\avoids the WindowsApps desktopClaude.exetree;< todayavoids killing the live/interactive session.Expected fix remains: each scheduled headless run should exit when its work completes.
Same accumulation on 2.1.219, but with two differences from the original report that may point at a second code path — recording them here rather than opening a duplicate.
Environment: Windows 11 Home (10.0.26200), Claude Code 2.1.219 via the desktop app, 32 logical cores, 63.8 GB RAM. Two recurring scheduled tasks fire daily (07:35 and 07:57).
The accumulation reproduces exactly. Every scheduled-task firing leaves one headless
claude.exe --output-format stream-jsonsession resident after the run completes and writes its output. Observed across four consecutive days — both daily tasks park a session every time, and nothing reaps them:| | day 1 | day 2 | day 3 |
|---|---|---|---|
|
claude.exeprocesses | 28 | 34 | 48 || total working set | ~5.1 GB | 9.42 GB | 13.97 GB |
| sessions older than 24h | 15 | 15 | 25 |
| free RAM (of 63.8 GB) | — | 27.9 GB | 20.6 GB |
Oldest resident session at the time of writing: 90.8 hours. The only thing that has ever cleared them is a restart (or killing them by hand, which is what I ended up doing — 50 processes / 14.12 GB down to 26 / 6.34 GB).
Difference 1 — no
--resumeThe original report describes
claude.exe --resume --output-format stream-jsonworkers. I checked the full command line of all 35 resident sessions on this machine: zero carry--resume, and zero carry--continue. They are plain--output-format stream-json --input-format stream-jsondesktop-app sessions. So the leak is not specific to the--resumepath.Difference 2 — they are not idle
The original report says the leaked workers "sat at ~0% CPU forever". These do not. Sampling every parked session's
UserModeTime + KernelModeTimetwice over a 30-second window:So these are not hung at exit — they are sitting in some polling loop indefinitely. That is a meaningfully different end state from "finished, then hung", and may be the more useful thread to pull: something is keeping the event loop alive rather than the process failing to tear down.
Other things checked, in case they narrow it
claude.exe(one parent process owned 29 of them). The app is holding them, not leaking detached children.~/.claude/projects/<cwd-key>/<session-uuid>.jsonl, so the resident process is not the only copy of the conversation. Useful for anyone hitting this: you can restart the app without losing conversation history, only warm context. Verified — transcript count and total size were identical before and after killing 25 sessions.settings.jsonto turn on.Happy to pull anything else off this machine — it reproduces daily without any effort on my part.
Still present in Claude Desktop 1.24012.9 / bundled Claude Code CLI 2.1.219 (Windows 11 Pro, build 26200).
Data from ~15.5 h of app uptime with two scheduled tasks (one every 20 min, one hourly):
%APPDATA%\Claude\claude-code\2.1.219\claude.exe --output-format stream-json --input-format stream-json ...; consumed real CPU during its run, then sat permanently idle — consistent with the OP's stdin-EOF diagnosisOne finding that may help others writing workarounds (and possibly the fix itself): headless scheduled-run workers always carry
--disallowedTools AskUserQuestionon their command line; interactive session hosts never do. That allows a precise reaper that cannot touch interactive sessions:We run this hourly via Task Scheduler as a stopgap; happy to share the full script.
+1 for an idle timeout in headless mode, or closing the worker's stdin when the run's completion handler fires.
Confirming this on macOS as well — it is not Windows-specific.
Setup: Claude desktop app (bundled claude-code 2.1.221) with a scheduled task on a 5-minute cron. Each run spawns a headless worker (
claude --output-format stream-json --input-format stream-json --permission-prompt-tool stdio ...) that completes its work but never exits.Observed: 78 leaked processes accumulated over ~7.5 hours (one per scheduled run, none ever exited). Unlike the idle-RSS case in the OP, each lingering worker also kept burning ~3–6% CPU indefinitely, so together they held ~4 cores — enough to push
kernel_taskthermal throttling to ~300% and make the machine hot and barely usable until they were killed.Notes:
SIGTERMcleanly terminates the leaked workers.Environment: macOS (Darwin 25.6.0), claude-code 2.1.221, desktop app 1.25927.0.
Note: the analysis below was written by Claude Code, running on the affected machine; I'm posting it on its behalf. Same convention as @khenderson-asurity's comment above.
Reproduces on macOS — this is not
platform:windowsSame bug, same mechanism, on macOS 26.6.1 (Apple M2 Max, 32 GB) with claude-code 2.1.229 / Claude Desktop 1.30096.5 — newer than any version corroborated so far in this thread. Suggest widening the
platform:windowslabel.Three recurring tasks (two hourly, one
*/30) = 4 fires/hour. Over ~22 hours:| | |
|---|---|
| Resident
claudeworkers | 90 || Combined RSS | 23.4 GB |
| Load average (1-min) | 138.84 |
|
kernel_task| 120.8% || Free RAM | 44 MB |
| VM compressor | 14.5 GB |
| Swap | 7.2 GB of 8.2 GB |
macOS degrades differently from the Windows freeze: instead of OOM it thrashes the VM compressor, so
kernel_taskpins a core doing nothing but compression and the machine becomes unusable while never actually running out. Easy to misdiagnose as thermal throttling.Caught one live: finished in 18 seconds, resident 24 minutes later
Another that finished at 06:03 was still alive 24 hours later. Every transcript from the killed batch ends with a proper final summary plus a
last-promptterminator — these runs all succeeded. Nothing is hung at the task level.Direct confirmation of the stdin-EOF mechanism
@yurukusa's reasoning above is correct, and it's directly observable on macOS. fd 0 of a lingering worker is a still-connected unix socket back to the desktop app, not a closed pipe:
sampleshows the process parked inkevent64under the main thread — an idle event loop with a live handle, exactly as predicted. The launcher (Claude.app→Contents/Helpers/disclaimer→claude) never closes that socket, so the worker never sees EOF.Platform difference worth flagging: on macOS the leaked workers do not carry
--resumeThis inverts the Windows discriminator. @khenderson-asurity confirmed the Windows leaked workers do carry
--resume. On macOS it's the opposite — checked across four concurrent processes:Anyone porting the PowerShell workaround from this thread to macOS will kill their live sessions if they filter for
--resume. Scheduled runs here are spawned fresh with no session to resume; interactive ones are always resumed.For anyone building a reaper: process age and CPU are both unsafe filters
@yurukusa already flagged that age-based filters catch legitimately-busy workers. Worth adding that CPU usage doesn't separate them either — the idle event loop still ticks. Measured over 40 s:
| process | CPU delta | actually |
|---|---|---|
| leaked worker | 0.37 s | idle, finished 20+ min ago |
| working session | 0.50 s | mid-task |
| working session | 0.52 s | mid-task |
0.9%/s vs 1.3%/s is far too close to threshold on.
The signal that does work is transcript write recency. A working agent appends to its
~/.claude/projects/<cwd>/<uuid>.jsonlon every message and tool call; a finished one never writes again. The worker's transcript can be matched to its PID by birth time (stat -f %Bwithin ~90 s of process start — the leaked run's transcript was born 1 second after the process). Backstop it with a child-process check to cover a single long-running tool call, where there are no transcript writes but there is a child.Impact
This is silent and there's no UI surface for it — the first symptom is the machine becoming unusable. Users who rely on scheduled routines and leave the machine on overnight will hit it every time. An idle timeout in headless stream-json mode, or simply closing the child's stdin once the run's final result has been emitted, would fix it.