Library / SDK
fsnotify/fsnotify avatar
fsnotify/fsnotify

fsnotify: cross-platform filesystem notifications in Go

Cross-platform filesystem notifications for Go.

10,784 stars988 forksGoBSD-3-Clause

At a glance

What is it?
fsnotify wraps inotify, kqueue, ReadDirectoryChangesW and FEN behind one Go API. It is a small library with a large surface of platform-specific behaviour, and the README is honest about where it stops.
Who is it for?
Adopt fsnotify if you are writing Go and need to react to file changes on more than one operating system, and if you are willing to add watches per directory yourself. Do not adopt it if your files live on NFS, SMB, FUSE, /proc or /sys, because the README states notifications do not work there, and no polling backend exists yet.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 142 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 27, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What fsnotify solves, and who ends up using it

Every operating system exposes file change notifications through a different API and a different set of quirks. Linux has inotify, BSD and macOS have kqueue, Windows has ReadDirectoryChangesW, and illumos has FEN. Writing against all four means writing four backends and keeping them consistent. fsnotify is the Go library that does that work: it presents one Watcher type, one Events channel and one Errors channel, and picks the right backend at build time. The repository layout confirms this, with backend_inotify.go, backend_kqueue.go, backend_windows.go, backend_fen.go and a backend_other.go fallback sitting at the top level.

The audience is Go developers building tools that react to the filesystem: config reloaders, live-reload servers, log shippers, build tools, and anything that needs to notice a file changed without polling. It is a library, not a daemon. There is no server to run and no configuration file to maintain. The only runtime dependency declared in go.mod is golang.org/x/sys, and the module requires Go 1.23.0 or newer.

Because it is a library, the interesting decisions are pushed onto the caller. You decide what to watch, how to debounce, and what to do about the events that arrive in bursts. fsnotify gives you the raw signal and very little policy.

How the Watcher, Events and Errors channels fit together

The mechanism is a fan-in. When you call fsnotify.NewWatcher() you get a Watcher, and on Linux that maps to one inotify instance. Each call to watcher.Add(path) registers a watch on that path with the kernel backend. The backend goroutine reads raw kernel events, translates them into fsnotify.Event values, and pushes them onto the Events channel. Failures that are not tied to a single path go to the Errors channel instead.

Events carry a Name and an Op bitmask. The README's example checks event.Has(fsnotify.Write) to detect modification, which tells you the Op field is a set of flags rather than a single enum value. Both channels are closed when the watcher is closed, and the README's example handles that by checking the ok return value before using the event. If you skip that check you will process a zero-value event on shutdown.

The design is deliberately low level. There is no debouncing, no event coalescing, and no recursive watch. The README states plainly that subdirectories are not watched and that you must add watches for any directory you want to watch, with a recursive watcher listed as roadmap item #18. For a directory tree with thousands of folders, that means thousands of Add calls, and on Linux it means thousands of inotify watches.

Installing fsnotify and watching a directory for the first time

fsnotify installs as a normal Go module. Run this inside your module to add it to go.mod and download the source:

bash
go get github.com/fsnotify/fsnotify

The README's basic example is the shortest path to a working watcher. It creates a watcher, starts a goroutine that drains Events and Errors, adds /tmp, and then blocks. Note the defer watcher.Close() and the ok checks on both channels:

go
watcher, err := fsnotify.NewWatcher()
if err != nil {
    log.Fatal(err)
}
defer watcher.Close()

go func() {
    for {
        select {
        case event, ok := <-watcher.Events:
            if !ok {
                return
            }
            log.Println("event:", event)
        case err, ok := <-watcher.Errors:
            if !ok {
                return
            }
            log.Println("error:", err)
        }
    }
}()

err = watcher.Add("/tmp")
if err != nil {
    log.Fatal(err)
}

Change /tmp to a directory you control, then touch or edit a file inside it and you should see event lines on stdout. The repository also ships a runnable command, which the README says can be started with `go run ./cmd/fsnotify`. That command is the better first stop if you want to see raw event output before writing your own handler, and cmd/fsnotify/file.go contains the file-watching example the README points to.

Watching files instead of directories is the mistake to avoid

The README answers this one directly: watching individual files is generally not recommended. The reason is atomic replacement. Many editors write a temporary file and then move it over the original, so the inode or file the watcher was attached to no longer exists at that path. The watch is lost silently, and your program keeps running while receiving nothing.

The recommended pattern is to watch the parent directory and filter on Event.Name. That is what cmd/fsnotify/file.go demonstrates. It costs you a small amount of filtering code and saves you from a class of bug that only appears when a user saves from a particular editor.

There is a second file-level trap on Linux, documented in the platform notes. When a file is removed while a file descriptor is still open, no REMOVE event is emitted until the descriptor closes, and a CHMOD is emitted in the meantime. The README gives the exact sequence: open the file, remove it, and you get CHMOD; close it, and you get REMOVE. Code that treats CHMOD as harmless metadata churn will miss the deletion entirely.

Where fsnotify does not work at all

Network and virtual filesystems are out of scope. The README states that notifications do not work with NFS, SMB, FUSE, /proc or /sys, because fsnotify requires support from the underlying operating system and those filesystems do not provide it. The only fix named in the README is a polling watcher, tracked as issue #9 and not yet implemented.

If your application watches a mounted network share, fsnotify is the wrong tool and no configuration will change that. You need a polling loop of your own, or a different library that already implements polling. This is not a gap that a version bump will close soon: the issue is open and the README lists polling as "Not yet" alongside fanotify on Linux 5.9+, FSEvents on macOS and USN Journals on Windows.

Resource limits are the second hard boundary. On Linux, fs.inotify.max_user_watches caps watches per user and fs.inotify.max_user_instances caps instances per user, and the README notes that every Watcher you create is an instance while every path you add is a watch. Hitting either limit produces "no space left on device" or "too many open files". On macOS and BSD the constraint is different: kqueue needs a file descriptor per watched file, so a directory with five files costs six descriptors, and you reach kern.maxfiles or kern.maxfilesperproc faster than you expect. A recursive watcher over a large tree on macOS is a descriptor budget problem, not a CPU problem.

Raising inotify limits on Linux before you ship

Because the default watch limits vary per distribution and per available memory, a watcher that works on a developer laptop can fail on a smaller production host. The README gives the sysctl commands to raise both values at runtime:

bash
sysctl fs.inotify.max_user_watches=200000
sysctl fs.inotify.max_user_instances=256

To persist those across reboots, the README points at /etc/sysctl.conf or /usr/lib/sysctl.d/50-default.conf, with the exact file depending on the distribution. The values are written as plain key-value lines in that file. The same numbers are readable at /proc/sys/fs/inotify/max_user_watches and /proc/sys/fs/inotify/max_user_instances, which is the quickest way to check what a host is actually configured for before you deploy.

On macOS and BSD the equivalent knobs are kern.maxfiles and kern.maxfilesperproc. The README names them without giving values, which is fair, since the right number depends on how many files your tree contains.

fsnotify compared with polling and with language-level watchers

The obvious alternative is polling: walk the tree on a timer and compare modification times. Polling works on NFS, SMB, FUSE and /proc, which is exactly where fsnotify cannot help. It also has no watch limit to hit and no file descriptor cost per file. The trade-off is latency and load. A one-second poll means up to one second of delay and a full tree walk every second, which is wasteful on large trees and on machines where the files rarely change. fsnotify gets an event the moment the kernel sees the change and costs nothing while the filesystem is idle.

The second alternative is a higher-level Go watcher built on top of fsnotify, typically adding recursion, debouncing and ignore patterns. Those libraries exist because fsnotify deliberately stops at the raw event layer. Choosing one means accepting its debounce policy and its ignore syntax. Choosing fsnotify means writing that policy yourself, which is more work but leaves you in control of exactly which events reach your handler. Neither is wrong; the question is whether your project wants to own that logic.

One comparison worth noting from the search data: people ask about fanotify versus fsnotify. They are different layers. fanotify is a Linux kernel interface, and fsnotify is a Go library that uses inotify on Linux. The README lists a fanotify backend as not yet supported.

Licence, maintenance and upgrade cost

fsnotify is BSD-3-Clause. That is a permissive licence, so it can be used in closed-source products, but the usual obligations apply: keep the copyright notice and the licence text with redistributed source or binary forms, and do not use the project's name to endorse your product without permission. This is a description of the licence text, not legal advice; read LICENSE in the repository if the distinction matters to your organisation.

The repository is not archived, and the last push was on 2026-05-11. Releases v1.10.0 and v1.10.1 landed in late April and early May 2026, after v1.9.0 in April 2025. The dependency footprint is one module, golang.org/x/sys, which keeps upgrade risk low.

Upgrade cost is dominated by the kernel backends, not by the API. The go.mod file carries retract directives for v1.5.0 and v1.5.3, one for a symlink regression and one for an incorrect branch published by accident. That history is a reminder to pin a version and read CHANGELOG.md before bumping, rather than tracking main. Behaviour differences between Linux and macOS are a property of the underlying kernels, so a version bump will not make kqueue behave like inotify.

Editorial conclusion

Adopt fsnotify if you are writing Go and need to react to file changes on more than one operating system, and if you are willing to add watches per directory yourself. Do not adopt it if your files live on NFS, SMB, FUSE, /proc or /sys, because the README states notifications do not work there, and no polling backend exists yet. Before you commit, check the inotify watch limits on your Linux hosts with sysctl fs.inotify.max_user_watches, and check kern.maxfiles on macOS and BSD, because every path you add consumes a watch or a file descriptor.

Frequently asked questions

What is fsnotify?

It is a Go library that provides cross-platform filesystem notifications on Windows, Linux, macOS, BSD and illumos, wrapping inotify, kqueue, ReadDirectoryChangesW and FEN behind one API.

What is an fsnotify watcher?

A Watcher is the object returned by fsnotify.NewWatcher(). It exposes an Events channel and an Errors channel, and each call to watcher.Add(path) registers a watch on that path with the operating system backend.

How does fanotify compare with fsnotify?

fanotify is a Linux kernel interface, while fsnotify is a Go library that uses inotify on Linux. The README lists a fanotify backend as not yet supported.

Official sources

  1. fsnotify/fsnotify on GitHub
  2. Issues
  3. License: BSD-3-Clause
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/fsnotify-fsnotify.svg)](https://hysenlabs.com/projects/fsnotify-fsnotify)