#1466·chokidar

Windows: false change events for watched files when many sibling files are written

Author: okorionCreated Jun 18, 2026Updated Jun 25, 2026

Windows: false change events for watched files when many sibling files are written

Describe the bug

On Windows, chokidar can emit high-level change events for watched files even when the watched files' mtimeMs, ctimeMs, and size have not changed.

I can reproduce this by watching many files directly and then writing generated sibling files in the same directory. In the example below, chokidar watches 300 *.scss files and then writes 300 sibling *.scss.d.ts files next to them.

The generated *.scss.d.ts files are not watched paths, but chokidar still emits change for the watched *.scss paths.

Versions

  • Chokidar version: 5.0.0
  • Node version: v22.14.0
  • OS version: Windows_NT 10.0.19045
  • Platform: win32
  • Filesystem: local Windows filesystem

To reproduce

bash
npm install [email protected] --save-exact
node repro.mjs --files=300 --wait=2500 --rewrite-on-change=false
repro.mjs
javascript
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
import { createRequire } from 'node:module';
import chokidar from 'chokidar';

const require = createRequire(import.meta.url);
const chokidarEntry = require.resolve('chokidar');
const chokidarPkgPath = path.join(path.dirname(chokidarEntry), 'package.json');
const chokidarPkg = JSON.parse(fs.readFileSync(chokidarPkgPath, 'utf8'));

const args = new Map(
  process.argv.slice(2).map((arg) => {
    const [key, value = 'true'] = arg.replace(/^--/, '').split('=');
    return [key, value];
  }),
);

const fileCount = Number(args.get('files') ?? 300);
const waitMs = Number(args.get('wait') ?? 2500);
const workspace = path.resolve(args.get('workspace') ?? './tmp-watched');
const watchedDir = path.join(workspace, 'watched');
const rewriteOnChange = args.get('rewrite-on-change') !== 'false';

fs.rmSync(workspace, { recursive: true, force: true });
fs.mkdirSync(watchedDir, { recursive: true });

const files = [];
const previousStats = new Map();

for (let i = 0; i < fileCount; i += 1) {
  const file = path.join(watchedDir, `file${i}.scss`);
  fs.writeFileSync(file, `.class-${i} { color: red; }\n`);
  const stat = fs.statSync(file);
  files.push(file);
  previousStats.set(file, {
    mtimeMs: stat.mtimeMs,
    ctimeMs: stat.ctimeMs,
    atimeMs: stat.atimeMs,
    size: stat.size,
  });
}

let rawScssEvents = 0;
let rawGeneratedEvents = 0;
let highLevelAddEvents = 0;
let highLevelChangeEvents = 0;
let generatedWritesFromChange = 0;
const samples = [];

function eventPathKind(value) {
  const normalized = String(value ?? '').replaceAll('\\', '/');
  if (normalized.endsWith('.scss.d.ts')) return 'generated';
  if (normalized.endsWith('.scss')) return 'scss';
  return 'other';
}

const watcher = chokidar.watch(files, { ignoreInitial: false });

watcher.on('raw', (_event, eventPath) => {
  const kind = eventPathKind(eventPath);
  if (kind === 'scss') rawScssEvents += 1;
  if (kind === 'generated') rawGeneratedEvents += 1;
});

watcher.on('add', () => {
  highLevelAddEvents += 1;
});

watcher.on('change', (changedPath) => {
  highLevelChangeEvents += 1;
  const stat = fs.statSync(changedPath);
  const previous = previousStats.get(changedPath);

  if (samples.length < 5) {
    samples.push({
      file: path.basename(changedPath),
      previous,
      current: {
        mtimeMs: stat.mtimeMs,
        ctimeMs: stat.ctimeMs,
        atimeMs: stat.atimeMs,
        size: stat.size,
      },
      mtimeUnchanged: previous?.mtimeMs === stat.mtimeMs,
      ctimeUnchanged: previous?.ctimeMs === stat.ctimeMs,
      sizeUnchanged: previous?.size === stat.size,
    });
  }

  if (rewriteOnChange) {
    fs.writeFileSync(`${changedPath}.d.ts`, 'export {};\n');
    generatedWritesFromChange += 1;
  }
});

await new Promise((resolve) => watcher.on('ready', resolve));

for (const file of files) {
  fs.writeFileSync(`${file}.d.ts`, 'export {};\n');
}

await new Promise((resolve) => setTimeout(resolve, waitMs));
await watcher.close();

console.log(JSON.stringify({
  environment: {
    platform: process.platform,
    osType: os.type(),
    osRelease: os.release(),
    node: process.version,
    chokidar: chokidarPkg.version,
  },
  result: {
    rawScssEvents,
    rawGeneratedEvents,
    highLevelAddEvents,
    highLevelChangeEvents,
    generatedWritesFromChange,
  },
  samples,
}, null, 2));

Observed result without rewrite amplification

With --rewrite-on-change=false:

json
{
  "rawScssEvents": 0,
  "rawGeneratedEvents": 0,
  "highLevelAddEvents": 300,
  "highLevelChangeEvents": 300,
  "generatedWritesFromChange": 0
}

Sample watched file stats from one high-level change event:

json
{
  "file": "file0.scss",
  "previous": {
    "mtimeMs": 1781776903285.5947,
    "ctimeMs": 1781776903285.5947,
    "atimeMs": 1781776903285.5947,
    "size": 26
  },
  "current": {
    "mtimeMs": 1781776903285.5947,
    "ctimeMs": 1781776903285.5947,
    "atimeMs": 1781776903285.5947,
    "size": 26
  },
  "mtimeUnchanged": true,
  "ctimeUnchanged": true,
  "sizeUnchanged": true
}

Observed result with rewrite amplification

If the consumer writes changedPath + '.d.ts' in response to each high-level change, the false positive can feed back into the same sidecar-file workflow:

bash
node repro.mjs --files=300 --wait=2500
json
{
  "rawScssEvents": 0,
  "rawGeneratedEvents": 0,
  "highLevelAddEvents": 300,
  "highLevelChangeEvents": 4517,
  "generatedWritesFromChange": 4517
}

Expected behavior

If a watched file's mtimeMs, ctimeMs, and size are unchanged, writing sibling files should not emit a high-level change event for that watched file.

Additional context

This came up in a generated sidecar-file workflow: watch *.scss, then write generated *.scss.d.ts files next to them. A single false change can make the consumer write the generated file again, which creates another burst of sibling writes and amplifies the loop.

This does not look like a glob issue. The repro passes an explicit array of watched *.scss file paths to chokidar.

If this is something chokidar should handle, I am happy to follow up with a small PR. I would probably start with a regression test, then try a conservative stats guard for unchanged watched files.