inotify and File Watching Explained

inotify and File Watching Explained

inotify is a kernel subsystem that tells a program when a file changes. Without it, a program wanting to react to file changes has to poll, checking timestamps in a loop, which wastes CPU and adds latency.

Your editor reloading a file changed on disk, a development server rebuilding on save, and Nextcloud noticing a new file all use it.

The watch limit

The thing most people meet first, as an error:

Error: ENOSPC: System limit for number of file watchers reached
cat /proc/sys/fs/inotify/max_user_watches
# 65536

That is a per-user limit, and each watched directory consumes one.

The reason it is hit so readily: inotify is not recursive. Watching a directory tree means placing a watch on every directory in it. A JavaScript project with node_modules can contain tens of thousands of directories, and a development server watching it consumes tens of thousands of watches on its own.

Add an editor watching the same tree, a file sync client, and a second project, and 65536 is gone.

# raise it
echo 'fs.inotify.max_user_watches=524288' | sudo tee /etc/sysctl.d/99-inotify.conf
sudo sysctl --system

Each watch costs roughly 1KB of kernel memory, so 524288 watches is about 512MB in the worst case. In practice nothing approaches that, and 524288 is a common recommendation.

Three related limits:

cat /proc/sys/fs/inotify/max_user_instances     # inotify instances per user
cat /proc/sys/fs/inotify/max_queued_events      # events queued per instance

Our sysctl guide covers making these permanent properly.

Finding what is consuming them:

find /proc/*/fd -lname anon_inode:inotify 2>/dev/null |
  cut -d/ -f3 | sort | uniq -c | sort -rn | head

Then map the PIDs to processes with ps. The answer is usually an editor or a file sync client, and occasionally something that has leaked watches through a bug.

Using it from the command line

sudo apt install inotify-tools
inotifywait -m /var/log
inotifywait -m -r -e modify,create,delete /srv/data
inotifywait -m -e close_write --format '%w%f' ~/watched

-m monitors continuously rather than exiting on the first event. -r is recursive, which is where the watch count comes from.

close_write is usually the event you want, not modify. A program writing a file generates many modify events as it goes, and one close_write when it finishes. Acting on modify means acting on a partially written file.

inotifywait -m -e close_write --format '%w%f' ~/incoming |
while read -r file; do
    echo "processing $file"
    process "$file"
done

A common mistake is using -e create, which fires when the file appears, before any content is in it.

The events:

EventMeaning
accessRead
modifyWritten
close_writeClosed after writing
createCreated in a watched directory
deleteDeleted
moveRenamed
attribMetadata changed

The editor problem

Many editors do not write files in place. They write a temporary file and rename it over the original, which is the safe pattern because a rename is atomic and a crash mid-write cannot corrupt the original.

The consequence for watchers: you get moved_to rather than modify or close_write, and the original watch is now on a deleted inode.

inotifywait -m -e close_write,moved_to,create /path/to/file

Watching the directory rather than the file handles this correctly, which is what well-written tools do.

Our symlinks and hard links guide covers the inode model this depends on.

Overflow

Events queue per instance. If the reader is too slow and the queue fills, the kernel drops events and sets IN_Q_OVERFLOW.

A correct program checks for that flag and rescans the tree rather than assuming it has a complete picture. A program that does not will silently miss changes under load, which produces bugs that only appear on busy systems.

This is worth knowing when evaluating a tool: if it watches files and does not handle overflow, it is unreliable at scale.

fanotify

The more capable and less known interface.

Two things it does that inotify cannot:

Monitor an entire mount point with one watch. No recursion problem, no watch limit, no cost proportional to directory count.

fanotify_mark(fd, FAN_MARK_ADD | FAN_MARK_MOUNT,
              FAN_OPEN | FAN_CLOSE_WRITE, AT_FDCWD, "/home");

Intercept access before it completes. With FAN_OPEN_PERM, the kernel blocks the opening process until your program responds with allow or deny.

That is how antivirus on-access scanning works, and how some audit and DLP systems work. inotify tells you something happened; fanotify can prevent it.

The cost is privilege: fanotify needs CAP_SYS_ADMIN for most of its useful modes, so it is for system services rather than user applications.

sudo apt install fatrace
sudo fatrace                     # every file access, system-wide
sudo fatrace -f W                # writes only
sudo fatrace -c                  # current directory

fatrace is genuinely useful for “what is writing to this disk constantly”, which is otherwise awkward to answer. Our lsof guide covers the complementary question of what has a file open right now.

Where it does not work

Network filesystems. inotify only sees changes made through the local kernel. A file changed by another NFS or SMB client produces no local event. Tools needing to detect remote changes must poll, which is why file sync on a network share behaves differently from a local directory.

Some virtual filesystems. /proc and /sys generate limited or no events.

Containers and bind mounts. Events generally work, and there are edge cases with overlay filesystems where changes in a lower layer do not propagate as expected.

Very large trees. The watch cost is per directory, so watching a million-directory tree is impractical regardless of the limit. fanotify on the mount point is the answer there.

Practical uses

# reload a service when its config changes
inotifywait -m -e close_write /etc/myapp/config.yml |
while read -r _; do
    systemctl reload myapp
done

A systemd path unit does this more cleanly, using inotify underneath:

# myapp-reload.path
[Path]
PathChanged=/etc/myapp/config.yml

[Install]
WantedBy=multi-user.target
# myapp-reload.service
[Service]
Type=oneshot
ExecStart=/bin/systemctl reload myapp

That is the better pattern for anything long-running: systemd supervises it, it survives reboots, and there is no shell loop to keep alive.

For development, entr is the pleasant wrapper:

ls *.go | entr -r go run main.go
find . -name '*.py' | entr -c pytest

-r restarts the process on change, -c clears the screen first.

Frequently Asked Questions

What is inotify?

It is a kernel subsystem that notifies a program when files or directories change, replacing the need to poll. A program registers watches on specific paths and reads events from a file descriptor when something happens, which uses far less CPU than repeatedly checking timestamps.

Why do I get an error about the inotify watch limit?

Each watched directory consumes one watch from a per-user limit, commonly 8192 or 65536 by default. Editors, file sync clients, and development servers each watch thousands of directories, so running several together exhausts the limit. Raise max_user_watches with sysctl.

Does inotify watch subdirectories automatically?

No. inotify is not recursive, so a tool watching a directory tree must place a separate watch on every directory in it. This is why watching a node_modules directory can consume tens of thousands of watches and why the limit is hit so easily.

What is the difference between inotify and fanotify?

inotify watches specific paths and reports what changed. fanotify can monitor an entire mount point with a single watch and can intercept access before it completes, allowing a program to permit or deny it. fanotify needs elevated privileges and is what antivirus and audit tools use.

Does inotify work on network filesystems?

Not reliably. inotify only sees changes made through the local kernel, so a file modified by another client on NFS or SMB produces no event locally. Tools that need to detect remote changes on a network share have to fall back to polling.

Can events be lost?

Yes. Events are queued per watch descriptor, and if the queue fills because the reader is too slow, the kernel drops events and sets an overflow flag. A well-written program checks for that flag and rescans rather than assuming it has seen everything.