Cowork scheduled tasks on Windows leave claude.exe processes alive after completion, causing memory accumulation
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Note: this report is about Claude Cowork (research preview, Windows desktop), not the Claude Code CLI. Filing here because Cowork is built on Claude Code / Agent SDK infrastructure and there's no dedicated Cowork repo.
On Windows, Cowork scheduled tasks appear to leave their claude.exe worker processes alive after the task prompt finishes. Over time these accumulate and consume significant RAM and committed memory. Manually killing all claude.exe processes fully reclaims the memory, confirming none of them are still doing useful work.
After ~24 hours of operation with several active schedules I observed:
- 16
claude.exeprocesses alive simultaneously - ~4.3 GB total working set across those processes
- Process start times spanning the previous 24 hours, no gaps corresponding to task completions
- Killing every
claude.exereclaimed ~7.7 GB committed and ~3 GB physical RAM in one shot - Same leak does not occur from interactive chat sessions — only scheduled-task invocations
Diagnostic detail: each claude.exe and its descendants (Node MCP host, child shells) live in a Windows Job Object with LimitFlags = 0x3C00 (DIE_ON_UNHANDLED_EXCEPTION | BREAKAWAY_OK | SILENT_BREAKAWAY_OK | KILL_ON_JOB_CLOSE). The KILL_ON_JOB_CLOSE flag means the OS would reap the whole job once the job handle is released — so the leak is upstream of the OS: something in Cowork's scheduled-task lifecycle is keeping the worker claude.exe (and therefore the job handle) alive after the task prompt has returned.
What Should Happen?
When a Cowork scheduled task finishes executing its prompt, the claude.exe worker process that was spawned for that task should exit and release its memory back to the OS. The steady-state claude.exe process count and total memory footprint should remain roughly constant regardless of how many scheduled tasks have fired over the previous day.
Error Messages/Logs
No error is surfaced by Cowork — the leak is silent. The only observable signal is external: the `claude.exe` process count grows over time and total working-set/committed memory climbs into the multiple-GB range.
Observed snapshot from `Get-Process claude`:
- 16 processes alive at once after ~24 h
- Earliest process start time matched the start of the day; newest matched the most recent scheduled-task invocation
- After killing all `claude.exe` processes and letting Cowork relaunch normally: 10 fresh processes, ~2.2 GB total — i.e. 16 - 10 = 6 truly orphaned workers carrying ~2 GB of dead weight.
Steps to Reproduce
- On a Windows machine running Cowork, configure several scheduled tasks at different cron intervals — for example one every 30 minutes, one hourly during business hours, one daily early-morning, and one weekly. Five to ten tasks is enough to show the pattern clearly.
- Let the machine run uninterrupted for 12 to 24 hours.
- Periodically run
Get-Process claudein PowerShell and track the process count and total working-set memory:
``powershell``
Get-Process claude | Measure-Object WorkingSet64 -Sum |
Select-Object Count, @{n='TotalMB';e={[math]::Round($_.Sum/1MB,1)}}
- Observe that the process count grows monotonically over time and total memory climbs. Expected: roughly flat. Observed: linear growth in both.
- Confirm the leak is on Cowork's side, not the OS, by killing every
claude.exeprocess (Get-Process claude | Stop-Process -Force) — the memory is reclaimed immediately and the next normal Cowork launch returns to a small steady-state footprint.
Claude Model
None
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
Claude for Windows build 1.8555.2 (a476c3)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
PowerShell
Additional Information
Current workaround
A daily scheduled kill of all claude.exe processes restores normal memory usage. Not ideal as a long-term solution because it briefly interrupts any in-progress interactive session and forces a relaunch.
Suggested investigation
- Verify the scheduled-task runner is awaiting the completion handler and exiting the worker
claude.exeafter the task prompt returns. - Check whether errors during MCP server teardown (e.g., a connector tool failing to disconnect cleanly, or an MCP child still doing I/O) prevent the worker from exiting and leave the process spinning idle. Several of my scheduled tasks talk to MCP servers (Microsoft 365, CRM, local desktop shell) and the leak rate seems proportional to how many tasks invoke MCP tools.
- Consider an idle-timeout safety net: if a scheduled-task worker has been alive for more than N minutes past its prompt completion, terminate it.
Impact
For users running several Cowork scheduled tasks on a workstation, this silently consumes several GB of RAM per day. On lower-spec laptops this is enough to noticeably degrade overall responsiveness within a day or two of normal use.
Showing cached comments. Read the full discussion on GitHub ↗
7 Comments
Issue Summary
Title: Cowork scheduled tasks on Windows leave
claude.exeprocesses alive after completion, causing memory accumulationLabels: bug, has repro, platform:windows, perf:memory, area:cowork
Competition: 0 comments (zero competition)
Priority: 🔴 High — memory leak affecting long-running Cowork deployments
Root Cause Analysis
The diagnostic information in the issue is critical:
claude.exeand its descendants live in a Windows Job Object withLimitFlags = 0x3C00KILL_ON_JOB_CLOSEflag is set, meaning the OS would reap the whole job once the job handle is releasedclaude.exe(and therefore the job handle) alive after the task prompt has returnedThis means the process lifecycle management code is not calling the cleanup/teardown path after a scheduled task completes. The job handle reference is being held, preventing the OS from cleaning up the process tree.
Likely Culprits
CloseHandle()on the Job Object handle — the scheduled task manager holds a reference to the job handle after task completionclaude.exeworker's event loop continues running after the task prompt returns (no exit signal sent)Proposed Fix
Fix 1: Explicit Process Termination on Task Completion (Primary)
In the scheduled task completion path, ensure the worker process is explicitly terminated:
Fix 2: Periodic Orphan Process Sweep (Defense in Depth)
Add a background sweep that detects and cleans up orphaned
claude.exeprocesses:Fix 3: Job Object Handle Lifecycle Management
Ensure the Job Object handle is scoped to the task lifecycle:
Testing Methodology
claude.exeprocess count grows beyond active task countImpact Assessment
KILL_ON_JOB_CLOSEflag already ensures cleanup on handle closeCompetitive Advantage
has reprolabel means the bug is well-documentedThis is a classic process lifecycle management gap — not a one-off bug. When scheduled task workers don't have a parent process monitor with a hard kill timeout, zombie processes are inevitable across any LLM runtime on Windows.
Diagnostic matrix for anyone hitting this:
CreateJobObject+JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, every scheduled run is a leak risk.wmic process where "name='claude.exe'" get ProcessId,ParentProcessId,CreationDateafter a scheduled task completes. If orphans remain with dead parent PIDs, you're seeing the root cause.Stop-Process -Name claude -Forceas cleanup.I've been hardening multi-agent deployments on Windows and this exact pattern comes up repeatedly. Happy to share the process-lifecycle checklist we use if it'd help.
(Not affiliated with Anthropic — just deep in the agent ops trenches.)
This is a place where the process tree needs a hard cleanup contract. On Windows, tying the scheduled task to a job object and explicitly closing the child tree on completion or cancel would keep the parent from accumulating zombie work. A visible exit receipt would make leaks obvious during test runs.
Cross-reference: I filed #66647 earlier today (now closed as a dup of this). Claude (the assistant doing the search) missed this issue when checking for prior reports — it searched on
"stream-json","does not exit","cron", and"scheduled task"keywords and surfaced #1920, #24478, #24481, #25629, but not #62107. Apologies for the noise; the dup bot caught it correctly.Posting the additional diagnostic from #66647 here since it complements what's already on this thread.
Process is stuck on stdin EOF, not on any internal work
Captured on a freshly-leaked process about 2 minutes after the cron tick (Windows 11,
claude.exe2.1.165, parent = Claude desktop main):Waitstate. Wait-reason histogram isUnknown× 20 (Node libuv parked onGetQueuedCompletionStatus) +EventPairLow× 5 +UserRequest× 2 +Executive× 1.conhost.exe. No hungwsl,kubectl,bash,python, etc.So it's not a hung tool call, not a stuck MCP server inside the worker, not a deadlocked promise — it's just the libuv event loop waiting on stdin that the parent never closes.
The smoking-gun command line
From
Win32_Process.CommandLineon a leaked cron-spawned process:--input-format stream-jsonwith no termination signal flag. The scheduler writes the prompt as stream-json frames to stdin, consumes the response, then keeps stdin open. No EOF → libuv has nothing to drain → process parks forever.Likely-related side effect:
scheduled-tasks.jsonunbounded growthThe scheduler's
recordedSkipsarray grows by one entry per minute per task whenever it skips a fire due toper_task_limit— which it does because it (correctly!) sees the prior run as "still alive." Observed 1,599 stale entries / 128 KB in%APPDATA%\Claude\claude-code-sessions\<id>\<sub>\scheduled-tasks.jsonafter a few days. Plausibly the same root cause: fix the exit and the skip-spam goes away.Suggested fixes (ranked)
recordedSkipsaccumulating.resultframe in--input-format stream-jsonwhen stdin is non-TTY. Behind a flag (--exit-on-result) to preserve current behavior for long-lived sessions.(1) is highest-leverage IMO.
Workaround used in the meantime
End-of-turn detached self-kill from the SKILL.md prompt — delay long enough for the assistant's final stream-json frames and any
notifySessionIdcallback to flush:Or a Windows-side janitor scheduled task that kills
claude-code\*\claude.exeidle > N minutes. Both are band-aids.June 18th release notes claim this bug is now resolved in build 1.14271.0
<img width="626" height="584" alt="Image" src="https://github.com/user-attachments/assets/e13120a9-f4fd-4984-8073-0e9b98585294" />
Corroborating this on Windows 10 (build 19045 / 22H2) and on much newer versions — it still reproduces on Claude Code 2.1.219 and 2.1.221, so this should probably lose the
stalelabel.I posted a detailed writeup on #71424 (the macOS report of what appears to be the same bug): https://github.com/anthropics/claude-code/issues/71424#issuecomment-5194707293
What matches this issue exactly:
5-55/10 * * * *(every 10 minutes)claude.exealive after the run's transcript is complete and its final report is writtenclaude --versiondied with a Bun stack overflow, and PowerShell'sConvertFrom-JsonthrewOutOfMemoryExceptionon an 8 KB fileDiagnostic detail that may narrow the cause. A leaked process whose run has demonstrably finished still holds established connections to the API:
And the control group that makes it sharp: on one day, 58 of 91 scheduled runs died early on
API Error: Unable to connect to API (ENOTFOUND)during two network outages, and every one of those exited cleanly. All 33 runs that did real work leaked. Work => leak, no work => clean exit. The early-failing runs never resolved DNS, so they never established the API connection pool — which points at the parent's own undrained keep-alive connections rather than at MCP children holding resources.Cross-referencing three reports that look like one bug:
| Issue | Platform | Scale |
|---|---|---|
| #62107 (this one) | Windows, Cowork scheduled tasks | 16 processes, 4.3 GB / 24 h |
| #71424 | macOS, local-agent sessions + scheduled tasks | 50+ processes, GBs |
| #77459 | Windows 11, desktop app sessions | 100 processes, 14.66 GB, 23 h+ survival |
Worth highlighting that #77459 is on Windows 11 while this report and mine are Windows 10, and #71424 is macOS — so this is not tied to a Windows version or to one OS at all.
One more point from #77459 that I can independently confirm: updating the CLI does not help, because the Desktop app ships its own bundled CLI on a separate update channel. On my machine
~/.local/bin/claudewas 2.1.204 while the app was running 2.1.219 —claude updatemoved only the former.Full details, plus a reaping workaround that reliably distinguishes leaked scheduled runs from live interactive sessions (via
~/.claude/sessions/<PID>.jsonplus the session transcript), are in my #71424 comment linked above.Still reproducing on current builds, ~3 months after this was filed. Adding a fresh data point plus one detail that may help scope the fix.
Environment
mcp__scheduled-tasks, cron*/15across staggered daily windowsSnapshot after one morning (~2h45 of firings)
| | |
|---|---|
| Total
claude.exe| 28 processes / 5,379 MB || Older than 20 min | 23 processes / 3,247 MB |
| Confirmed-idle runner trees reaped | 11 trees / 2,622 MB reclaimed |
Oldest leaked runner had been alive 2h44m past its firing, at 0 active CPU. Process start times map 1:1 onto cron fires (07:49, 08:04, 08:19, 08:34, 08:49, 09:02, 09:08, 09:25, 09:38, 09:49, 10:08) with none missing — every fire leaks, none self-reap.
Confirming the original report's key asymmetry: interactive sessions in the same app do not leak. My own
%LOCALAPPDATA%-installed CLI sessions exited normally throughout, and the Electron helper processes (--type=renderer,--type=gpu-process,--type=utility) are all correctly parented and reaped. Only the scheduled-task runners accumulate.Current command line still has no exit-on-result path
Captured today from a live leaked runner (trimmed):
Unchanged from what @cellison-hashbrowns captured on 2.1.165 in #66647.
The parent appears to have enough information to know the session is one-shot
--disallowedTools AskUserQuestionis present on every leaked scheduled-task runner and absent from every interactive session, on this machine and (per #80885) on macOS too. That flag is the runner's own declaration that no human will ever answer this session — which is exactly the condition under which stdin can be closed after the finalresultframe without breaking anything.Worth flagging because the flag set here is otherwise correct:
--input-format stream-jsonis required for the live--permission-prompt-tool stdiochannel and for--include-partial-messages, so this isn't a case of a wrong flag being passed. The gap is purely that nothing signals EOF once the unattended turn is done. That seems narrower than the job-handle path described in the original report, and possibly independent of it.Workaround for anyone else hitting this
Filtering on the CLI path plus the stream-json signature avoids killing the Electron helpers or your own interactive sessions, and
/Ttakes the MCP children (which otherwise survive as orphans, cf. #71424):The 20-minute floor spares an in-flight run. Adjust it above your longest task duration.