# @parcel/watcher: native filesystem events and history queries for Node

> @parcel/watcher is a C++ Node addon that both subscribes to recursive filesystem change notifications and answers queries about what changed since a saved snapshot, across macOS, Linux, Windows and FreeBSD. It is MIT licensed and is what Parcel 2 itself uses, and the query side is the part most tools do not offer.

**parcel-bundler/watcher** — 👀 A native C++ Node module for querying and subscribing to filesystem events

- Repository: https://github.com/parcel-bundler/watcher
- Stars: 843 · Forks: 72
- Language: C++
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/parcel-bundler-watcher

## Two jobs: live subscriptions and historical queries

@parcel/watcher is a native C++ Node module for querying and subscribing to filesystem events, used by Parcel 2. The interesting thing about it is not the watching half, which every platform provides some way of doing, but the querying half.

The watch side is the familiar one. You subscribe to a directory and get callbacks when files or directories are created, updated or deleted, and it works recursively, so changes in subdirectories are emitted too. The query side is what separates this from a plain fs.watch wrapper. You can ask what changed in a directory since a snapshot you saved earlier, even if your program was not running at the time. The documentation's stated use for this is invalidating a cache and rebuilding only the files that changed, and it notes the query can be significantly faster than traversing the entire filesystem to determine what changed, depending on the platform.

That is a real capability with a real limit. It requires a previous snapshot, which you save with writeSnapshot, ideally just before your program exits. Without a snapshot there is no baseline, so on first run you are still doing a full traversal.

The feature list frames the rest of the design. Watch for realtime recursive notifications. Query for historical events. Native, implemented in C++ for performance and low-level integration with the operating system. Cross platform, with backends for macOS, Linux, Windows, FreeBSD and Watchman. Performant, because events are throttled in C++ so the JavaScript thread is not overwhelmed during large filesystem changes such as a git checkout or npm install. And scalable, with tens of thousands of files watched or queried at once.

The target audience is build tools and dev servers, and specifically anyone who has written the same cache invalidation code twice.

## The event semantics have edges you will hit

The documentation is precise about what events you actually receive, and the details matter more than usual because a watcher that under-reports is worse than one that over-reports.

Events are throttled and coalesced for performance during large changes like git checkout or npm install, and a single notification is emitted with all of the events at the end. So your callback is not called once per file. It is called once with a batch, after the burst settles.

Only one notification is emitted per file. If a file was both created and updated since the last event, you get only a create event. If a file is both created and deleted, you will not be notified of that file at all. Renames cause two events, a delete for the old name and a create for the new name.

That last rule is the one to design around. A file created and deleted inside a single throttle window vanishes, and if your build system only rebuilds on create, you will miss it. The same applies to a file that was written and then removed, or to an editor that writes a temporary file and renames it into place, since that produces a delete plus a create for what is logically one file.

Each event has two properties. The type is create, update or delete, and the path is the absolute path to the file or directory. The path being absolute rather than relative to the watched root is worth noting, since it means you cannot assume a relative path comparison without resolving it first.

The subscription object has an unsubscribe method you await when you are done:

```javascript
await subscription.unsubscribe();
```

The full example from the README shows the shape of a subscription and a query in the same file:

```javascript
const watcher = require('@parcel/watcher');
const path = require('path');

// Subscribe to events
let subscription = await watcher.subscribe(process.cwd(), (err, events) => {
  console.log(events);
});

// later on...
await subscription.unsubscribe();

// Get events since some saved snapshot in the past
let snapshotPath = path.join(process.cwd(), 'snapshot.txt');
let events = await watcher.getEventsSince(process.cwd(), snapshotPath);

// Save a snapshot for later
await watcher.writeSnapshot(process.cwd(), snapshotPath);
```

The callback signature with an err first argument and events second is the older Node style, and it is worth noting that events are delivered through it rather than through a promise.

## Five watch backends, chosen by priority or forced by name

The watch backends are listed in priority order, and the order itself explains the design.

FSEvents is first, the macOS system facility. Watchman comes next, if it is installed. Then inotify on Linux, ReadDirectoryChangesW on Windows, and kqueue on FreeBSD, or as an alternative to FSEvents on macOS.

The reason Watchman sits so high is that it is a background daemon that already tracks changes, and a client can query its cache rather than starting a new watch. The documentation says as much in the querying section: the FSEvents and Watchman backends are significantly more performant than the brute force backends used by default on Linux and Windows, for example returning results in milliseconds instead of seconds for large directory trees, because a background daemon monitoring filesystem changes on those platforms allows cached data to be queried rather than traversing the filesystem manually.

So on Linux and Windows, a watcher with no daemon is watching with inotify or ReadDirectoryChangesW and will not be able to answer historical queries cheaply. That is a deployment decision, not just a performance tweak: if your tool depends on the query side, Watchman has to be on the machine.

You can override the selection. Passing the backend option chooses a backend by name, and the allowed values are fs-events for FSEvents, watchman, inotify, kqueue, windows and brute-force, where brute-force applies only to querying. If the named backend is not available on the current platform, the default is used instead, so a forced backend never fails outright.

The query backends are a shorter list: FSEvents on macOS, Watchman if installed, fts brute force on Linux and FreeBSD, and FindFirstFile brute force on Windows.

On macOS performance is good with FSEvents by default. For the best performance elsewhere, the recommendation is to install Watchman, which will then be used automatically.

## Querying across restarts and the snapshot file

The query workflow is two calls, one to write the snapshot and one to read the difference.

You save a snapshot of a directory to a file with writeSnapshot, which the documentation suggests doing just before your program exits:

```javascript
await watcher.writeSnapshot(dirPath, snapshotPath);
```

Then at startup you ask what changed since that snapshot:

```javascript
let events = await watcher.getEventsSince(dirPath, snapshotPath);
```

The events returned are exactly the same as the events that would be passed to the subscribe callback, which is the property that makes the feature composable. A build tool can share one event handler between its live watching path and its startup catch-up path, and the semantics you coded against for one hold for the other.

The two APIs have the same options, and all of them are passed as an object as the last function argument. The ignore option takes an array of paths or glob patterns, and it uses is-glob to distinguish paths from globs, with the glob patterns themselves parsed by picomatch. Paths can be relative or absolute and can be files or directories, and no events are emitted about those files or directories or their children. Glob patterns match on relative paths from the watched root.

The relative-to-root matching for globs is the detail to notice. A glob in ignore is not matched against an absolute path, so writing node_modules works while writing an absolute path to a node_modules directory does not behave the same way, and a pattern anchored with a leading slash will not match what you expect. The same rules apply to both subscribe and getEventsSince, which is convenient, and a mistake in an ignore pattern silently removes events rather than raising an error.

The second option is backend, the explicit backend name described above.

## Building the addon and what the repository contains

The package is published as @parcel/watcher with the version 2.6.0, the main entry at index.js, type definitions in index.d.ts and a Flow declaration in index.js.flow. The engines field asks for Node 10.0.0 or newer, and the binary section declares N-API version 3, which is what lets one prebuilt binary work across Node versions.

The build is standard node-gyp territory, with a binding.gyp at the root and the native sources in src. The scripts show the release path: prebuildify with napi and strip and a tag-libc builds the prebuilt binaries, build runs node-gyp rebuild for a local compile, install runs node scripts/build-from-source.js, and test runs mocha.

The install script is the interesting one. It decides whether to build from source, which is what you want on an unusual platform and what the dependencies detect-libc suggests: the package is aware that prebuilt binaries are not always usable, for instance on a musl based Linux distribution, and it can fall back to compiling.

The runtime dependencies are small and mostly JavaScript. detect-libc, is-glob, node-addon-api and picomatch. The dev dependencies add esbuild, fs-extra, husky, lint-staged, mocha, napi-wasm, prebuildify and prettier.

The src directory holds the backend implementations, and the Makefile in the repository root reveals their names: BruteForceBackend.cc, legacy.cc for the Unix kqueue path and a WasmBackend.cc. The Makefile also builds a WASM variant using emscripten with a list of exported functions and an initial memory setting of 524288000 bytes.

There is a watchman directory and a wasm directory, and a wrapper.js at the root that is what index.js and the other published entry points sit on top of.

## The WASM fallback and where the trade-offs sit

There is a companion package, @parcel/watcher-wasm, that can be used in place of @parcel/watcher on unsupported platforms. It relies on the Node fs module, so in non-Node environments such as browsers, an fs polyfill will be needed. The README also notes that the WASM implementation is significantly slower, though the sentence is cut off in the version available here, so the magnitude of that difference is not stated.

The performance gap is not surprising given the architecture. The native path talks to inotify or ReadDirectoryChangesW or FSEvents directly. The WASM path goes through a JavaScript fs polyfill inside a WebAssembly sandbox, which means the events have to be reconstructed by polling or by hooking the polyfill rather than by the kernel telling you what happened.

The whole design has one central trade-off that is worth stating plainly. Watching is cheap and imprecise, and querying is precise but needs a daemon and a snapshot. The brute force backends on Linux and Windows answer a query by walking the tree and comparing, which is correct but slow on a large directory, and the documentation is candid that this is where the milliseconds versus seconds difference comes from. A tool that only watches never pays that cost, because it never asks a historical question.

The alternative approaches are worth naming because they are different in kind. Node's own fs.watch is the built-in option, and it is famously inconsistent across platforms, with different semantics for rename, duplicate events and recursive watching on different systems, and it has no history at all. Chokidar is the higher level JavaScript library that most Node tooling uses, and it wraps fs.watch and adds normalisation, glob support and a polling fallback, which gives you consistent semantics but keeps you in JavaScript. Watchman itself is the daemon that chokidar can delegate to, and it is where the real indexing capability lives, so a tool that needs both live events and fast historical queries ends up in the same place this library does.

The difference between @parcel/watcher and chokidar is roughly that this one is a thin native layer over the platform facilities and Watchman, exposing a small API with the two capabilities either side of it, while chokidar is a JavaScript abstraction that normalises behaviour. For a build tool where correctness of the event stream matters more than API breadth, the native approach is defensible.

## Conclusion

Adopt @parcel/watcher if you need a Node build tool or dev server to react to filesystem changes and, more importantly, to ask what changed since the last run so it can invalidate a cache instead of rescanning the tree. Do not adopt it in a browser, since the WASM fallback needs an fs polyfill and is significantly slower. Verify first that you understand the event semantics, because a file created and deleted between two notifications produces no event at all, and confirm that a Watchman daemon is available on Linux and Windows if query performance matters to you, since the brute force fallback traverses the filesystem.

## FAQ

### What is @parcel/watcher used for?

It is a native C++ Node module for querying and subscribing to filesystem events, used by Parcel 2. It supports realtime recursive change notifications and querying for historical changes since a saved snapshot, so a tool can rebuild only what changed instead of traversing the whole tree.

### Which backends does @parcel/watcher use for watching?

In priority order: FSEvents on macOS, Watchman if installed, inotify on Linux, ReadDirectoryChangesW on Windows, and kqueue on FreeBSD or as an alternative to FSEvents on macOS. You can force one with the backend option, whose allowed values are fs-events, watchman, inotify, kqueue, windows and brute-force.

### What happens if a file is created and then deleted between two notifications?

You are not notified of that file. The documentation states that only one notification is emitted per file, so a create followed by an update gives only a create, and a file that is both created and deleted produces no event at all. Renames produce a delete for the old name and a create for the new name.

### Why do I need Watchman installed for fast queries?

On Linux and Windows the default query backends are brute force, fts and FindFirstFile, which traverse the filesystem and are significantly slower than the FSEvents and Watchman backends. With Watchman installed the module can query a background daemon's cached data instead, returning results in milliseconds rather than seconds on large trees.

### Can I use @parcel/watcher in the browser?

Not directly. The @parcel/watcher-wasm package can be used in place of @parcel/watcher on unsupported platforms, but it relies on the Node fs module, so in browsers an fs polyfill is needed, and the README notes the WASM implementation is significantly slower.

## Sources

- [Issues](https://github.com/parcel-bundler/watcher/issues)
- [License: MIT](https://github.com/parcel-bundler/watcher/blob/master/LICENSE)
- [parcel-bundler/watcher on GitHub](https://github.com/parcel-bundler/watcher)
- [README](https://github.com/parcel-bundler/watcher/blob/master/README.md)
- [Releases](https://github.com/parcel-bundler/watcher/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/parcel-bundler-watcher
