Skip to content

Comments

fix(core): use recursive FSEvents on macOS instead of non-recursive kqueue#34523

Open
comp615 wants to merge 1 commit intonrwl:masterfrom
comp615:fix/macos-watcher-event-filter
Open

fix(core): use recursive FSEvents on macOS instead of non-recursive kqueue#34523
comp615 wants to merge 1 commit intonrwl:masterfrom
comp615:fix/macos-watcher-event-filter

Conversation

@comp615
Copy link
Contributor

@comp615 comp615 commented Feb 19, 2026

Current Behavior

Since Nx 22.5.0, the daemon's native file watcher silently drops all file change events on macOS in large monorepos (~5,250+ watched directories). nx watch, nx serve, and any daemon-dependent file watching is broken.

The root cause is that #34329 switched all watched paths to WatchedPath::non_recursive(). On macOS, the notify crate uses kqueue for non-recursive watches instead of FSEvents. kqueue silently fails at scale due to vnode table pressure (kern.num_vnodes == kern.maxvnodes), causing the daemon to never detect file changes.

This is a scale-dependent bug: it works fine in small workspaces (~30 directories) but breaks silently in large ones.

Nx 22.4.5 Nx 22.5.0+
Small repo (~30 dirs) Works (FSEvents) Works (~30 kqueue watches)
Large repo (~5,250+ dirs) Works (FSEvents) Broken (kqueue silently drops all events)

Expected Behavior

The macOS file watcher should detect file creates, modifications, and deletions at any scale, matching the behavior of Nx 22.4.x.

Fix

Use platform-conditional watch modes:

On macOS, FSEvents handles recursive watching from a single root path, so directory enumeration and dynamic registration are skipped entirely. This also improves daemon startup time on macOS from ~10 minutes to <1 second in a 354-project monorepo.

What changed in watcher.rs

  1. Initial pathset: On macOS, watch only the root directory recursively via FSEvents instead of enumerating all directories for non-recursive kqueue watches.
  2. Dynamic directory registration (on_action): Wrapped in #[cfg(not(target_os = "macos"))] since FSEvents already watches the full tree.

Linux/Windows behavior is completely unchanged.

Why the event filter is fine as-is

We verified that with recursive FSEvents watches, macOS emits specific FileEventKind variants (Create(File), Modify(Data(Content)), Remove(File), Modify(Name(Any))) that the current watch_filterer.rs already handles correctly. Zero events were rejected by the catch-all. The Modify(Any) / Create(Any) variants are kqueue artifacts that are not needed with FSEvents.

Why kqueue fails silently

Apple's File System Events Programming Guide explicitly recommends FSEvents over kqueue for large hierarchies: "If you are monitoring a large hierarchy of content, you should use file system events instead." kqueue requires open(path, O_EVTONLY) per watched directory. Under vnode table pressure, the kernel recycles vnodes with kqueue watches attached without notifying the watcher. There is no error, no partial delivery, and no diagnostic signal.

Tested on

  • macOS 26.3 (Tahoe), Apple Silicon (arm64), APFS
  • 354-project pnpm monorepo (~19,865 non-ignored directories)
  • Verified: file modifications, file creates, and file deletes all detected
  • Daemon init time: ~10 min (with enumeration) -> <1s (with root-only FSEvents watch)

Related Issue(s)

Fixes #34522

@comp615 comp615 requested review from a team as code owners February 19, 2026 21:28
@comp615 comp615 requested a review from FrozenPandaz February 19, 2026 21:28
@netlify
Copy link

netlify bot commented Feb 19, 2026

👷 Deploy request for nx-docs pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit c086f96

@netlify
Copy link

netlify bot commented Feb 19, 2026

👷 Deploy request for nx-dev pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit c086f96

@nx-cloud
Copy link
Contributor

nx-cloud bot commented Feb 19, 2026

View your CI Pipeline Execution ↗ for commit c086f96

Command Status Duration Result
nx affected --targets=lint,test,test-kt,build,e... ✅ Succeeded 46m 52s View ↗
nx run-many -t check-imports check-lock-files c... ✅ Succeeded 3m 49s View ↗
nx-cloud record -- nx-cloud conformance:check ✅ Succeeded 9s View ↗
nx-cloud record -- nx sync:check ✅ Succeeded <1s View ↗
nx-cloud record -- nx format:check ✅ Succeeded <1s View ↗

☁️ Nx Cloud last updated this comment at 2026-02-20 20:59:11 UTC

Copy link
Contributor Author

@comp615 comp615 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I feel like there might be a better refactor / solution here (e.g. pull them all up since they are common? Should FileEventKind::Modify(ModifyKind::Data(_)) => continue, even exist now?)...but I didn't have a full understanding and this is a narrow change. Feel free to build upon!

Copy link
Contributor

@nx-cloud nx-cloud bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

At least one additional CI pipeline execution has run since the conclusion below was written and it may no longer be applicable.

Nx Cloud has identified a possible root cause for your failed CI:

This CI failure appears to be related to the environment or external dependencies rather than your code changes.

No code changes were suggested for this issue.

You can trigger a rerun by pushing an empty commit:

git commit --allow-empty -m "chore: trigger rerun"
git push

Nx Cloud View detailed reasoning on Nx Cloud ↗


🎓 Learn more about Self-Healing CI on nx.dev

@comp615 comp615 marked this pull request as draft February 19, 2026 22:17
…queue

PR nrwl#34329 switched all watched paths to non-recursive to reduce inotify
watch count on Linux. On macOS, the notify crate uses kqueue for
non-recursive watches instead of FSEvents. kqueue silently fails at
scale (~5,250+ directories) due to vnode table pressure, causing the
daemon to never detect file changes in large monorepos.

This fix uses platform-conditional watch modes:
- macOS: single recursive watch on the workspace root (uses FSEvents)
- Linux/Windows: non-recursive per-directory watches (preserves nrwl#33781 fix)

On macOS, FSEvents handles recursive watching natively from a single
root path, so directory enumeration and dynamic registration are
skipped entirely. This also improves daemon startup time on macOS
(from ~10 minutes to <1 second in a 354-project monorepo).

Fixes nrwl#34522

Amp-Thread-ID: https://ampcode.com/threads/T-019c7804-5473-73ae-861f-d5dc1572a1ce
Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019c7804-5473-73ae-861f-d5dc1572a1ce
Co-authored-by: Amp <amp@ampcode.com>
@comp615 comp615 force-pushed the fix/macos-watcher-event-filter branch from 7cb2bda to c086f96 Compare February 20, 2026 05:24
@comp615 comp615 changed the title fix(core): expand macOS FileEventKind allowlist in daemon watcher fix(core): use recursive FSEvents on macOS instead of non-recursive kqueue Feb 20, 2026
@comp615 comp615 marked this pull request as ready for review February 20, 2026 05:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Native file watcher broken on macOS in large monorepos since 22.5.0 (kqueue scaling issue from #34329)

1 participant