Watching Filesystem Changes with inotify in Bash
AI generated
$_
#!/
Bash · Linux · inotify · Automation
Watching Filesystem Changes with inotify
Watch directories live without burdening the kernel with polling

inotify is a kernel interface that notifies Bash scripts the instant a file or directory changes, instead of checking again and again in a loop. With the command line tool inotifywait from the inotify-tools package, you can build deployment triggers, config reloads and log watchers that react to real events rather than a fixed time interval.

16 min read inotifywait · inotify-tools Bash · Linux · Kernel Events

1. What inotify is and why it beats polling

inotify is a Linux kernel interface that lets programs register interest in changes to specific files or directories and then block until the next event arrives, instead of actively asking again and again. The kernel already tracks every file operation internally, so the notification costs practically no extra work: the moment a process creates, modifies, moves or deletes a file, the kernel already knows, and simply forwards that information to registered watchers.

The alternative, a polling script that repeatedly calls stat in a loop or diffs a directory listing against an old snapshot, generates continuous load even when nothing changes for hours. Polling also trades its simplicity for latency: an event is noticed at the earliest on the next loop iteration, so with a five second interval it can be noticed almost five seconds late in the worst case. inotify, by contrast, reports the change essentially in real time.

2. Installing inotifywait and setting up a first watch

The command line tool inotifywait is not part of a default install, it comes from the inotify-tools package, installed on Debian and Ubuntu with apt install inotify-tools and on RHEL based systems with dnf install inotify-tools. That gives you a simple command line frontend over the underlying inotify syscalls, without having to write your own C program against the kernel API.

The simplest invocation, inotifywait /path/to/file, blocks until exactly one event occurs, prints the path and event type, then exits. For a long running Bash script the -m (monitor) flag is what matters: it keeps inotifywait running and prints every further change as its own line, so a Bash loop can process each line individually.


#!/usr/bin/env bash
set -euo pipefail

WATCH_DIR="/var/www/app/config"

# -m: monitor mode, keeps running instead of exiting after one event
# -e: restrict to the event types we actually care about
inotifywait -m -e modify,create,delete "$WATCH_DIR" |
while read -r directory events filename; do
  echo "Change detected: $events on $directory$filename"
done

3. Watching directories recursively and structuring output

By default inotifywait only watches the paths given directly, not their subdirectories. With the -r (recursive) flag, the tool internally registers a separate watch for every subdirectory, which noticeably slows startup for deep trees with many folders, but stays just as responsive as a single watch afterward.

For machine readable output, the --format option is worth using: it lets you print timestamp, event type and path in a format of your own choosing, for example tab separated. That avoids fragile parsing of the human readable default output and makes the Bash script more robust against paths containing spaces, especially when combined with null byte separation.


#!/usr/bin/env bash
set -euo pipefail

WATCH_DIR="/var/www/app/uploads"

inotifywait -m -r -e create,moved_to \
  --format '%T|%e|%w%f' --timefmt '%Y-%m-%dT%H:%M:%S' \
  "$WATCH_DIR" |
while IFS='|' read -r timestamp event path; do
  printf 'time=%s event=%s path=%s\n' "$timestamp" "$event" "$path"
done

4. Triggering Bash reactions on specific events

The real value shows up once the loop reacts differently to individual event types instead of treating every change the same. A deployment watcher, for instance, should reload a service on close_write, meaning after a write is fully finished, but do nothing on a plain open event, because the file is not even fully written at that point.

That is exactly why close_write is the better choice for most automation use cases than modify: modify fires on every single write call, even in the middle of a large copy operation, while close_write only fires once the writing process actually closes the file and the content is consistent.


#!/usr/bin/env bash
set -euo pipefail

CONFIG_FILE="/etc/myapp/app.conf"

inotifywait -m -e close_write "$(dirname "$CONFIG_FILE")" |
while read -r directory events filename; do
  if [[ "$filename" == "$(basename "$CONFIG_FILE")" ]]; then
    echo "Config fully written, reloading service"
    systemctl reload myapp.service
  fi
done

5. Limits: inode watch limits and event queue overflow

Every inotify watch occupies a fixed amount of kernel memory, and the number of simultaneous watches per user is capped by fs.inotify.max_user_watches, often only eight to sixteen thousand by default. Anyone watching a very large directory such as a full node_modules tree with tens of thousands of files with -r will quickly hit that limit, and inotifywait aborts with an error rather than silently watching only part of the tree.

There is also a queue for events not yet picked up, capped by fs.inotify.max_queued_events. If the Bash loop processes events slower than the kernel produces them, for example because it runs an expensive action per line, that queue fills up, and on overflow events are permanently lost without the script automatically learning about it, unless it specifically checks for the Q_OVERFLOW event.


# Show and raise the relevant kernel limits (root required to persist)
sysctl fs.inotify.max_user_watches
sysctl fs.inotify.max_user_instances
sysctl fs.inotify.max_queued_events

# Temporary increase for the current boot
sudo sysctl -w fs.inotify.max_user_watches=524288

# Persist across reboots
echo "fs.inotify.max_user_watches=524288" | sudo tee /etc/sysctl.d/99-inotify.conf

6. Linux only: portability and container quirks

inotify is a pure Linux kernel feature and exists on neither macOS nor the BSD systems. Anyone writing a Bash script that also needs to run on macOS has to fall back on an alternative such as fswatch, which internally uses macOS's own FSEvents API but behaves similarly on the command line to inotifywait. A script that hard depends on inotifywait should check at startup whether the command even exists and otherwise fail cleanly.

Docker containers add another wrinkle: if a directory is bind mounted from a network filesystem or from certain overlay configurations, the host sometimes fails to forward inotify events to the container at all, or forwards them with significant delay, because the change happens outside the mountpoint the container actually observes. Watchers running inside containers are worth testing against the real deployment target, not just the local development environment.

7. inotify compared directly to a classic polling loop

A classic polling loop compares a stored state, for example a file's last modification time, against the current state at fixed intervals. The code for that is simple and works on any Unix like system without an extra package, but it pays for that simplicity with constant CPU usage and a reaction time that can never be better than the chosen sleep interval.

For rare, non-critical checks, for example whether a lock file still exists once a minute, polling remains the more pragmatic choice, because it avoids a dependency on inotify-tools. But once many events per second are expected, or millisecond level latency matters, for example a live reload during development, inotify is clearly the superior solution.


#!/usr/bin/env bash
set -euo pipefail

FILE="/var/lock/myapp.lock"
last_mtime=""

# Naive polling loop: wastes CPU cycles and reacts with up to 5s latency
while true; do
  if [[ -f "$FILE" ]]; then
    current_mtime="$(stat -c %Y "$FILE")"
    if [[ "$current_mtime" != "$last_mtime" ]]; then
      echo "Lock file changed"
      last_mtime="$current_mtime"
    fi
  fi
  sleep 5
done

8. Debouncing, error handling and running it long term

Many real world changes, for example an rsync sync or an editor with auto save, generate several events in quick succession for a single logical change. Without debouncing, a naive script triggers the same expensive action, say a service restart, several times in a row. A simple debounce technique collects events in a variable for a short window and only runs the action once no further events arrive for a defined span of time.

For long term operation, a watcher script belongs in a systemd service with Restart=on-failure, not an interactive terminal session, so a crashing or Q_OVERFLOW terminated inotifywait process gets restarted automatically. The script should also check inotifywait's exit code and log on failure instead of silently ending the loop.

9. When inotify pays off and when an alternative fits better

The choice between inotify, polling and higher level tools depends on event frequency, portability requirements and the acceptable reaction time. For local Linux automation with many events per second, inotifywait is nearly always the right choice, for portable scripts or rare checks it usually is not.

Tool Platform Resource usage Typical use case
inotifywait Linux only Very low, event driven Deployment triggers, config reload, live reload
Polling loop Everywhere (POSIX) Constant, regardless of activity Rare, non-critical checks without an extra package
entr Linux, macOS, BSD Low, event driven (inotify/kqueue) Developer watchers, running tests on file change
fswatch macOS, Linux, BSD Low, event driven Portable scripts spanning operating systems
systemd .path unit Linux only (systemd) Very low, built into the init system Service activation on file change without a custom script

Mironsoft

Shell automation, DevOps tooling and deployment infrastructure

Shell scripts that hold up in production?

We review existing Bash scripts, spot fragile patterns and replace them with robust Bash patterns: complete error handling, logging and safe parallelization for your deployment stack.

Code Review

ShellCheck analysis and manual review for critical Bash pattern violations.

Refactoring

Retrofitting error handling, logging and safe file operations.

CI Integration

Wiring ShellCheck and BATS into pipelines and building regression tests.

10. Summary

inotify in Bash: The Essentials at a Glance

Core idea

inotify reports filesystem events directly from the kernel, without a script having to actively ask.

Event types

Use close_write instead of modify so the reaction only fires after a write is fully finished.

Limits

fs.inotify.max_user_watches and max_queued_events cap the number of watches and the size of the event queue.

Portability

inotify only exists on Linux, fswatch is the practical alternative for macOS with a similar command line.

11. FAQ: inotify in Bash: The Essentials at a Glance

1What is inotify in simple terms?
inotify is a Linux kernel interface that informs programs about changes to files or directories as they happen, instead of the program having to check regularly itself.
2How do I install inotifywait?
Via the inotify-tools package, on Debian and Ubuntu with apt install inotify-tools, on RHEL based systems with dnf install inotify-tools.
3Why should I use close_write instead of modify?
modify fires on every single write call, even while a file is still being written. close_write only fires once the writing process closes the file and the content is consistent.
4How do I watch a directory including all subdirectories?
With the -r flag, inotifywait automatically registers a watch per subdirectory. With very many subdirectories that can hit the inode watch limit.
5What does the watch limit error mean?
It shows that fs.inotify.max_user_watches is exhausted. The value can be raised temporarily with sysctl -w fs.inotify.max_user_watches=524288 and permanently via /etc/sysctl.d.
6Does inotify work on macOS?
No, inotify is a purely Linux feature. On macOS, fswatch built on the FSEvents API plays a comparable role with a similar command line.
7What is Q_OVERFLOW and why does it happen?
It shows that the kernel event queue was full because the script processes events slower than they occur. Events are then permanently lost.
8When is a simple polling loop the better choice?
For rare, non-critical checks, when no dependency on inotify-tools is wanted, or when the script must also run on systems without inotify.
9How do I avoid triggering multiple times during an rsync run?
With debouncing: collect events in a short window and only start the actual action once no further events arrive for a defined span of time.
10How do I run an inotify watcher long term?
As a systemd service with Restart=on-failure, so a crashing or Q_OVERFLOW terminated inotifywait process is restarted automatically.