Repository navigation
⛔ 100% CPU on Windows when a watched file is deleted #287800
Description
Activity
Can you try to reproduce with our nightly insider builds? You can give our preview releases a try from: https://code.visualstudio.com/insiders/
- addedfile-watcherFile watcherFile watcherinfo-neededIssue requires more information from posterIssue requires more information from poster
on Jan 14, 2026 justanotheranonymoususer commented
on Jan 14, 2026 ContributorAuthorMore actionsYes, but only with installed ver, not portable.
Steps:- Install
- Create folder, open with vscode, trust it
- Open same folder in cmd
- Run:
C:\temp>mkdir .vscode && echo {"editor.fontSize": 33}>.vscode\settings.json C:\temp>rmdir /s /q .vscode\ && mkdir .vscode && echo {"editor.fontSize": 33}>.vscode\settings.json- 100% CPU for one core
- addedhelp wantedIssues identified as good community contribution opportunitiesIssues identified as good community contribution opportunitiesteam-low-hangingLow effort issues team members can help fix. No external contributions will be accepted.Low effort issues team members can help fix. No external contributions will be accepted.and removedinfo-neededIssue requires more information from posterIssue requires more information from poster
on Jan 14, 2026 Needs investigation if confirmed. Are you using any special file system or driver?
- addedfreeze-slow-crash-leakVS Code crashing, performance, freeze and memory leak issuesVS Code crashing, performance, freeze and memory leak issues
on Jan 14, 2026 justanotheranonymoususer commented
on Jan 14, 2026 on Jan 14, 2026 via emailContributorAuthorMore actionsNo, most standard setup. I suggest to try the repro, it takes as long as writing a comment.…On Wed, Jan 14, 2026, 19:09 Benjamin Pasero ***@***.***> wrote: *bpasero* left a comment (microsoft/vscode#287800) <#287800 (comment)> Needs investigation if confirmed. Are you using any special file system or driver? — Reply to this email directly, view it on GitHub <#287800 (comment)>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/ABMDRPCHNWJW6RMEBVM3HST4GZZ4LAVCNFSM6AAAAACRV4FN6OVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTONJQGYZDINBVG4> . You are receiving this because you authored the thread.Message ID: ***@***.***>Reacted by Benjamin Pasero- addedbugIssue identified by VS Code Team member as probable bugIssue identified by VS Code Team member as probable bugconfirmedIssue has been confirmed by VS Code Team memberIssue has been confirmed by VS Code Team memberand removed
on Jan 15, 2026 31 remaining items
- assigned and unassigned
on May 5, 2026 - added 10 commits that reference this issue
on Jul 10, 2026 Hit this in the wild today and captured forensics while the process was spinning — posting because it (a) confirms the repro class outside synthetic steps, and (b) shows why this recurs "randomly" for anyone using CLI coding agents, plus a question about shipping the fix.
Environment: VS Code 1.127.0 (user setup), Windows 11 10.0.26200, NTFS, no unusual filesystem drivers.
Symptoms (captured live, process had been spinning for ~1h):
- The fileWatcher utility process (
Code.exe --utility-sub-type=node.mojom.NodeService,watcher.nodeloaded) had one thread pegged at ~100% of a core, ~55 CPU-minutes accumulated. - ~301,000 "Other" I/O operations/second (consistent with a
ReadDirectoryChangesWretry loop), zero read/write operations, while no file in any watched tree had changed for minutes — pure spin, no event storm. - Sysinternals
handleshowed the process holding an open handle toC:\$Extend\$Deleted\<FRN>— i.e. a watched directory that was POSIX-deleted while the watcher held its handle. The five fileWatcher processes of my other open windows held zero such handles and were idle.
Trigger identified via NTFS USN journal: the
$Deletedentry corresponded to a directory underC:\Users\<user>\.claude\(agents/skills/rules— config dirs that VS Code extensions register watchers on). CLI coding-agent tooling (Claude Code and similar) scaffolds and permanently deletes these dirs as part of its session lifecycle — matching the finding in the linked Node issue that only permanent deletion (not Recycle Bin) triggers the loop. That explains the "happens eventually, seemingly at random" reports: any user running CLI agent tools alongside VS Code re-rolls this dice constantly. Killing the fileWatcher process recovers (VS Code respawns it); the spin otherwise persists indefinitely.On the fix: the upstream fix landed in libuv master on 2026-03-31 — libuv/libuv#5013 (
44125af6, "win: fix watch loop logic") — but as of today it is in no libuv release (latest v1.52.1 predates it) and has not been cherry-picked into Node'sdeps/uv, so no VS Code build including Insiders has it, and the release path libuv → Node 22.x → Electron → VS Code looks like several more months.Robo (@deepak1556) could
libuv/libuv@44125af6be cherry-picked into VS Code's Electron/Node runtime directly rather than waiting for the full release chain? Given how cheap the trigger is (delete a watched folder permanently), this seems worth shipping ahead of upstream.Reacted by justanotheranonymoususer and Jacob Sartin- The fileWatcher utility process (
Another field report, with a trigger that does not involve anyone deleting anything, plus the current shipping status of the libuv fix. The cherry-pick question Kamil (@KamilDev) raised in July is still open and there is a new data point on it at the bottom.
VS Code 1.133.0 (user setup), Windows 11 Pro 25H2 build 26200.9168, NTFS, 32 logical cores, no filesystem filter drivers beyond Defender.
An in-place installer is enough
I deleted nothing. Two ordinary product updates did it:
- Git for Windows 2.55.0.3 replaced
C:\Program Files\Git\cmd(replacement directory created 2026-08-16 20:19:50) - Docker Desktop 4.87.0 replaced
C:\Program Files\Docker\Docker\resources\bin(created 2026-08-17 20:40:42)
Both are on
PATH, VS Code held watch handles on both, so NTFS could not unlink them and relocated them toC:\$Extend\$Deleted\instead. That is the same zombie-handle state Stefan Stojanovic (@StefanStojanovic) described in nodejs/node#61398, reached with no user action that looks anything like a delete.Which makes the trigger routine rather than exotic. Any installer that replaces a
PATHdirectory in place re-rolls this for anyone with VS Code open, and on Windows that is a lot of installers.Why every window is hit at once
vscode.terminal-suggestis built in, on by default, and activates ononTerminalShellIntegration:*, so it comes up as soon as any terminal with shell integration starts.watchPathDirectories()inextensions/terminal-suggest/src/terminalSuggestMain.tsthen registers one non-recursive watcher perPATHentry, per window:const envPath = env.PATH; if (envPath) { envPath.split(delimiter).forEach(p => pathDirectories.add(p)); } for (const dir of pathDirectories) { try { const stat = await fs.promises.stat(dir); if (!stat.isDirectory()) { continue; } } catch { continue; } const watcher = vscode.workspace.createFileSystemWatcher( new vscode.RelativePattern(vscode.Uri.file(dir), '*')); ... }
The pattern is
'*', so these are non-recursive requests and land on the node.js watcher you identified (nodejsWatcherLib.ts), not on the parcel recursive watcher. And every window builds the same set, becausePATHis the same everywhere. One replacedPATHdirectory therefore produces one spinning watcher process in every open window.The shape is the problem: stat once, watch, done. No re-stat, no recovery path, nothing that reacts to the root going away. (#276477, merged 2025-11-10 as 5e5ba2f, is the change that moved these watches onto the shared watcher service.)
Measurements
Seven
Code.exe --type=utility --utility-sub-type=node.mojom.NodeServiceprocesses, all children of the same VS Code main process, sampled 2026-08-17 between 23:29 and 23:44 local:PID 45196 CPU 95,424 s threads 27 handles 381 WS 181 MB PID 45316 CPU 94,193 s threads 27 handles 381 WS 152 MB PID 45044 CPU 94,165 s threads 27 handles 381 WS 148 MB PID 45092 CPU 94,165 s threads 27 handles 387 WS 149 MB PID 45412 CPU 93,852 s threads 26 handles 379 WS 148 MB PID 30880 CPU 93,839 s threads 27 handles 381 WS 148 MB PID 45008 CPU 93,829 s threads 27 handles 381 WS 148 MBSeven other NodeService processes under the same parent at the same moment: 2.1 to 2.4 CPU seconds each.
The Git directory was replaced 27.3 hours before that snapshot. 94,000 CPU seconds over 27.3 hours is 95% to 97% of one core, sustained, per process. Times seven that is 6.7 cores of a 32-thread machine, standing, for over a day. No meaningful read or write I/O, just a continuous stream of metadata operations.
Handle evidence
All seven held exactly two directory handles under
$Extend\$Deleted, and it is the same two NTFS file references in every one of them:PID 45044: watchHandles=64 DELETED=2 \\?\C:\$Extend\$Deleted\001200000007826C14D88775 ; \\?\C:\$Extend\$Deleted\00F100000001B3A4480AB3C6 PID 45092: watchHandles=70 DELETED=2 (same two) PID 45412: watchHandles=65 DELETED=2 (same two) PID 45008: watchHandles=64 DELETED=2 (same two) PID 30880: watchHandles=64 DELETED=2 (same two) PID 45316: watchHandles=64 DELETED=2 (same two) PID 45196: watchHandles=64 DELETED=2 (same two) PID 74480: watchHandles=63 DELETED=0PID 74480 is the control. That window was opened after both installers had run, so it re-resolved
PATHat activation. It is idle, and it is the only watcher process still holding\\?\C:\Program Files\Git\cmd \\?\C:\Program Files\Docker\Docker\resources\binopen. Diffing the full directory-handle set of a sick watcher against that control, and ignoring the two workspace folders that differ because the windows have different folders open, the entire difference is:
only in healthy: C:\Program Files\Git\cmd C:\Program Files\Docker\Docker\resources\bin only in sick: C:\$Extend\$Deleted\001200000007826C14D88775 C:\$Extend\$Deleted\00F100000001B3A4480AB3C6A one-for-one substitution. So the sick watchers are not only spinning. They have also silently stopped watching the live Git and Docker directories, and nothing ever re-points them.
Loaded modules: 43 in the sick process, 43 in the control, and the two lists are identical. The only native addon in there is
@parcel/watcher, which is loaded in the same utility process for recursive workspace watching and is a red herring here. The spin is libuv'suv_fs_event, which matches the symbolised stack already posted in #263718.A reboot cleared it.
Developer: Reload Windowclears it. Killing the utility process clears it too, VS Code just respawns it.Repro
- Open three or more VS Code windows on Windows with defaults (
terminal.integrated.suggest.enabledis on). - Open an integrated terminal in each. That activates
vscode.terminal-suggestand registers thePATHwatchers. - Run an installer that replaces a directory already on
PATHin place. Git for Windows and Docker Desktop both qualify.
Without an installer:
mkdir C:\pathtest, add it to the userPATH, restart VS Code so terminal-suggest picks it up, open a terminal in each window, thenrmdir /s /q C:\pathtest && mkdir C:\pathtest. Permanent delete only, the Recycle Bin does not reproduce it, per Stefan Stojanovic (@StefanStojanovic).One NodeService utility process per window then climbs to roughly 100% of a core and stays there indefinitely with no file activity, and a handle enumerator shows the directory handles under
C:\$Extend\$Deleted\.Expected: the watcher notices its root is gone, fails, and
baseWatcherpoll-and-resume re-establishes the watch on the replacement directory.Actual: it spins on the orphaned directory object forever and never re-resolves.
Why the existing guard misses this
The suspend and resume machinery in
src/vs/platform/files/node/watcher/baseWatcher.ts(suspendWatchRequest(),monitorSuspendedWatchRequest(), polling atsuspendedWatchRequestPollingInterval = 5007) hangs entirely offonDidWatchFail, which is emitted bynotifyWatchFailed()innodejsWatcherLib.ts.In the zombie-handle case nothing fails.
ReadDirectoryChangesWkeeps returning success, libuv keeps deliveringrenameevents,fs.watchnever emitserror, sonotifyWatchFailed()is never called, the request is never suspended, and the resume poll never starts.Same reason #288003 does not help. As merged it added a re-entrancy guard inside
notifyWatchFailed(), which de-duplicates repeated failure notifications. There are no failure notifications here to de-duplicate.Shipping status of the fix, checked today
- win: fix watch loop logic libuv/libuv#5013 (
44125af6, "win: fix watch loop logic") merged 2026-03-31. In libuvmain,src/win/fs-event.cnow callsGetFinalPathNameByHandleW, tests the result for\$Extend\$Deleted\, and fireshandle->cb(handle, NULL, 0, UV_ENOENT)rather than re-arming. That is precisely theonDidWatchFailsignalbaseWatcheris sitting there waiting for. - Newest libuv release is still v1.52.1, published 2026-03-06, so it predates the merge. No libuv release contains the fix.
nodejs/node@mainhasdeps/uvpinned at 1.52.1, anddeps/uv/src/win/fs-event.chas no$Extend\$Deletedcheck and nodir_event_detectedas of today.- But Node does cherry-pick individual libuv commits straight into
deps/uv, and did exactly that to this very file four days ago:38e39556, "deps: cherry-pick libuv/libuv@e640dc9", deps: cherry-pick libuv/libuv@e640dc9 nodejs/node#65118, landed 2026-08-14.
So the route Kamil (@KamilDev) asked about in July is available and in current use, and
44125af6simply has not been picked up. The upstream Node issue was also stale-botted on 2026-07-20 and only stayed open because someone commented, so sitting on the libuv release chain has its own risk.Given that the trigger now includes any installer that replaces a
PATHdirectory, and that the cost multiplies by the number of open windows, this looks worth pulling forward rather than waiting for libuv release, then Node, then Electron, then VS Code.Options
Offered as options, not a prescription.
- Cherry-pick
libuv/libuv@44125af6into the Electron/Node runtime VS Code ships, the same way deps: cherry-pick libuv/libuv@e640dc9 nodejs/node#65118 pickede640dc9on 2026-08-14. Smallest change, fixes the root cause for every watcher. - Mirror the libuv check in
nodejsWatcherLib.tsas an interim guard. Capture the watched root's file ID at watch time, and on a burst of root-levelrenameevents resolve the handle's final path (or stat the root) and callnotifyWatchFailed()if it no longer matches. That handsbaseWatcherthe signal it already waits for, and recovery then happens by itself once the replacement directory appears. - Make
terminal-suggestre-resolvePATH. This one is independent of the CPU bug and survives the libuv fix: once libuv starts reportingUV_ENOENTthe spin stops, but nothing re-points the watcher at the new directory, so thatPATHentry's completions stay stale until reload. Filed separately as terminal-suggest: PATH is resolved once at activation, so a replaced PATH directory stops updating completions until reload #331457. - Deduplicate the
PATHwatch set across windows. It is identical in every window, so N windows register N copies of the same watches. Deduplicating them, or moving them to the shared process, cuts both the steady-state cost and the blast radius of any future watcher bug by the window count.
- Git for Windows 2.55.0.3 replaced
Being hit by this as well, and it makes things like collecting reliable benchmark figures via a run inside the VSCode terminal an exercise in futility. The numbers go all over the place due to one core being pegged at 100% utility by the spinning file watcher and reaching max thermal, followed by the entire CPU package downclocking.
Microsoft - how about putting some pressure on Node.js to get the libuv fix through the pipelines and to end-users and integrators?
Or god forbid- put your expertise to use on your own temporary fork of whatever dependencies require this fix to be transitively incorporated?"But it's not our bug to fix; it's upstream" is not something anyone should settle for, with an issue of this nature.
This is putting real, potentially dangerously real wear on actual silicon! Remember 13th-14th gen Intel chips and their lovely failure rates under constant high pressure? What do you think a runaway core kept at 100% utility for hours on end, without ever letting up save for thermal protection kicking in, is going to do to those chips? Nothing good, I'd say...
Does this issue occur when all extensions are disabled?: Yes
Version: 1.108.0 (user setup)
Commit: 94e8ae2
Date: 2026-01-08T13:53:10.781Z
Electron: 39.2.7
ElectronBuildId: 12953945
Chromium: 142.0.7444.235
Node.js: 22.21.1
V8: 14.2.231.21-electron.0
OS: Windows_NT x64 10.0.26200
I wrote about it in #263718, but couldn't get the team to reopen the issue. tl;dr if a watched file is deleted, then
ReadDirectoryChangesWis called in an infinite loop, causing 100% CPU. Happened several times for me.Steps to Reproduce:
.vscode\settings.jsonon windowsReadDirectoryChangesWis called for the path 2 times by the utility serviceReadDirectoryChangesWin an infinite loop.