Git's `fsmonitor` Protocol v2, Explained
What fsmonitor actually does, how the v2 wire protocol works between Git and a helper, and why it can make `git status` 100× faster without modifying Git itself.
Most articles about Git performance start with “set core.fsmonitor and your life will improve.” That’s true, but it leaves you wondering what the helper is actually doing and why it works. This piece walks through the protocol in detail so the next time you see it in the wild, you know exactly what’s flowing across the wire.
What problem fsmonitor solves
When you run git status, Git wants to know two things:
- Which tracked files have changed since the index was last written?
- Which untracked files exist in the working tree?
Without help, Git answers both by walking the working tree, calling lstat() on every file, and comparing each one’s mtime and size against the index. That walk is O(n) in the number of files. In a 250,000-file monorepo, even at 30,000 stat()s per second, that’s eight seconds before Git even starts comparing anything.
fsmonitor short-circuits the walk. It says: “Hey, helper — what’s changed since token T?” The helper, which has been listening to the OS’s file-change events the entire time, returns a tiny list. Git only stat()s those files. The walk goes from “every file” to “a handful of files,” and git status returns in tens of milliseconds.
The v1 protocol (legacy)
In v1, Git invoked the helper as a child process every time you ran git status. The helper’s stdin was the token; stdout was the changed file list. This worked, but:
- Every call paid fork/exec overhead — maybe 5–20ms on Linux, much worse on Windows.
- The helper had to persist watcher state somewhere external (a separate background process, or it would have to re-scan).
- There was no way to push a “watcher reset” notification to Git.
In practice, v1 still saved 100× over an unassisted status. But the fork/exec floor meant you couldn’t drop below ~20ms.
How v2 works
Version 2 keeps the helper as a long-lived daemon and uses an IPC channel. The transport differs by platform:
- Linux / macOS: a Unix domain socket at a well-known path (
.git/fsmonitor--daemon.ipcby default). - Windows: a named pipe in the local namespace.
Each request is a binary frame:
[version: u32] [token: variable-length null-terminated string]
Each response is:
[trivial-response: u8] # 0 = full list follows, 1 = "all clean", 2 = "trust nothing"
[token: variable-length null-terminated string]
[null]
[changed paths, each null-terminated]
[null]
The token is opaque to Git — it’s whatever the helper wants to use as a high-water mark. Most implementations use either an inotify cookie + sequence number, an FSEvents HFS event ID, or a ReadDirectoryChangesW USN. Whatever it is, it must be totally-ordered: if the helper hands token T2 back to Git, every subsequent call must use T2 or later.
The “all clean” response
If the helper knows nothing has changed since T, it returns response code 1 with no payload. Git, having received “all clean,” skips the working-tree walk entirely. This is the path that makes warm git status calls return in microseconds.
The “trust nothing” response
If the helper’s watcher was reset (e.g., the inotify queue overflowed and the helper had to restart), it returns code 2. Git falls back to a full walk — slow, but always correct. This is the safety valve that makes fsmonitor safe to enable: even when the helper gives up, the user-visible answer is still correct.
What a good helper does
A correct v2 helper has to handle a surprising amount of complexity:
- Watcher resets: when the OS reports an event queue overflow (this happens under heavy I/O), the helper must invalidate its token and tell Git to trust nothing on the next call.
- Repository changes: when the user does
git checkout, half the working tree mtime-changes at once. A good helper rate-limits its event queue so it doesn’t try to report a million file changes in one batch. - Token rotation: tokens must be totally ordered. If the helper restarts, it has to choose a new token namespace that sorts strictly higher than any token it previously issued.
- Index lock awareness: when Git is writing the index, the helper must not return cached results that may be invalidated by the in-flight write.
This is why “just enable fsmonitor” is harder than it sounds. The protocol is small; the correctness invariants are subtle. gity (and Watchman, and Microsoft’s own native helper) each represent thousands of lines of carefully tested daemon code.
Why a Rust daemon makes sense
Implementing a watcher daemon in a fast, memory-safe language gives you:
- Sub-millisecond IPC — no GC pauses, no JIT warm-up.
- Bounded memory — Rust’s ownership model makes leak-free long-running daemons straightforward.
- Cross-platform abstractions — crates like
notifygive you one API overinotify,FSEvents, andReadDirectoryChangesW. - A single binary — easy to ship via every package manager.
That’s the engineering thesis behind gity. The fsmonitor v2 protocol is a contract; a well-built helper just has to honour it faster than the alternatives.
What’s next
If you want to see the protocol in motion, run:
gity register .
strace -e network -p $(pgrep gity) # Linux
# or
sudo dtrace -n 'syscall::sendmsg:entry /execname=="git"/ { ustack(); }' # macOS
You’ll see Git dialing the helper for every status call and getting a tiny binary frame back in well under a millisecond. That’s the whole magic trick.
For the alternatives, read our comparisons: gity vs Watchman and gity vs native fsmonitor.
Frequently asked questions
What is Git's fsmonitor?
fsmonitor is a Git feature that lets a background helper process tell Git which files have changed since a given point in time. When Git would otherwise walk the entire working tree to compute status, it asks the helper instead and only scans the files the helper reports as changed.
What's new in fsmonitor v2?
Version 2 uses an IPC socket (Unix domain socket on Linux/macOS, named pipe on Windows) instead of spawning a hook on every call. The new protocol exchanges binary-framed messages and supports an `--ipc` mode where the helper is a long-lived daemon, eliminating fork/exec overhead and bringing latency below a millisecond.
Does fsmonitor work with stock Git?
Yes, since Git 2.37. Set `core.fsmonitor` to either `true` (use Git's built-in helper for some platforms) or a path to an external helper binary like gity. No patches, no forks — the protocol is part of upstream Git.