Skip to content

fs.watch (Windows): deleting the watched directory floods 'rename' events (~190k/s) forever instead of reporting EPERM #65837

Description

@smf-furuyama

Version

v24.13.1 (also reproduced with v22 via npx node@22)

Platform

Microsoft Windows NT 10.0.26200.0 x64 (Windows 11 Pro)

Subsystem

fs

What steps will reproduce the bug?

Watch a directory with fs.watch() (non-recursive), then delete that directory. The watcher starts emitting rename events with the watched directory's own path as fast as the event loop can run (~190,000 events per second), and never stops — not even after the directory is re-created with the same name.

// repro.mjs
import { watch, mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';

const dir = join(tmpdir(), `fswatch-repro-${process.pid}`);
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'a.txt'), 'x');

let count = 0;
const kinds = new Map();
const w = watch(dir, (event, file) => {
  count += 1;
  const k = `${event} ${file}`;
  kinds.set(k, (kinds.get(k) ?? 0) + 1);
});
w.on('error', (e) => console.log('error event:', e.code));
w.on('close', () => console.log('close event'));
const tick = (label) => console.log(label, count, [...kinds.entries()].slice(0, 3));

await new Promise((r) => setTimeout(r, 1000));
tick('before delete');
rmSync(dir, { recursive: true, force: true });
for (let i = 1; i <= 3; i += 1) { await new Promise((r) => setTimeout(r, 1000)); tick(`${i}s after delete`); }
mkdirSync(dir, { recursive: true });
for (let i = 1; i <= 3; i += 1) { await new Promise((r) => setTimeout(r, 1000)); tick(`${i}s after re-create`); }
w.close();

Run: node repro.mjs

How often does it reproduce? Is there a required condition?

Every time, on two machines' worth of runs (Node 24.13.1 and Node 22). The directory must be the watched directory itself (deleting a child file/directory behaves normally). NTFS local drive (C: and E:).

What is the expected behavior? Why is that the expected behavior?

Per the fs.watch caveats in the docs: "On Windows, no events will be emitted if the watched directory is moved or renamed. An EPERM error is reported when the watched directory is deleted." So either an error event (EPERM) and the watcher closing, or at most a single rename event for the directory — not an unbounded stream.

What do you see instead?

before delete 0 []
1s after delete 191393 [ [ 'rename a.txt', 1 ], [ 'rename \\\\?\\C:\\Users\\...\\fswatch-repro-21780', 191392 ] ]
2s after delete 379277 [ ... 379276 ]
3s after delete 574586 [ ... 574585 ]
1s after re-create 764369 [ ... 764368 ]
2s after re-create 952505 [ ... 952504 ]
3s after re-create 1138742 [ ... 1138741 ]

No error event, no close event — only rename events carrying the watched directory's own (\\?\-prefixed) path, ~190k/s, forever. One Node thread sits at 100% CPU.

Additional information

How we hit it: a Vite dev server (chokidar on top of fs.watch) was watching apps/*/coverage. Vitest's coverage run deletes and re-creates that directory. After that, the dev server used ~1.3 CPU cores continuously with nobody touching the app (measured with the inspector: 64% of samples in FSWatcher._handle.onchange → chokidar's handler; one FSWatcher handle emitted 306,559 rename events in 5 seconds). Two dev servers had accumulated 22 h and 9 h of CPU time before we noticed. Workaround on our side: exclude those directories from the watcher (server.watch.ignored).

process.versions.uv = 1.51.0. Related but different: #31702 (no event on move/delete), #25301 (Linux path), chokidar#1463 (watch stops after deletion — the opposite symptom).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions