Stop Polling a Folder Like It Owes You Money
Somewhere on your server there is a cron job that runs every minute, lists a directory, and finds nothing 1,439 times a day. It works. It also wakes up a shell, forks ls, and compares timestamps to answer a question the kernel already knows the answer to.
The kernel has a feature called inotify. It tells you the instant a file is created, written, moved, or deleted. The inotifywait command from the inotify-tools package puts that feature in a shell pipe. For the simpler job of “rerun this command when I save a file”, a tiny tool called entr does it in one line. Use inotifywait when your script needs to know which file changed and what happened to it. Use entr when you just want the same command to run again.
Full example: Clone the working files at github.com/KingPin/sumguy-examples/linux/inotifywait-shell-scripts
I ran every command below in a throwaway Debian trixie container with inotify-tools 4.23.9.0 and entr 5.7 (the Debian packages). As of November 2026 upstream is at inotify-tools 4.26.272 and entr 5.9, so your distro may be a bit behind. Everything here uses flags from the older versions, so you are fine either way.
(Systemd path units can also trigger a service on file changes. They get their own post.)
Install and Try It in 30 Seconds
sudo apt install inotify-tools entr # Debian/Ubuntusudo dnf install inotify-tools entr # Fedora (entr may need EPEL on RHEL clones)Open two terminals. In the first, watch a directory:
mkdir /tmp/demoinotifywait /tmp/demoIn the second, create a file:
touch /tmp/demo/hello.txtThe first terminal prints one event line and exits. That is the default behavior: wait for one event, report it, quit. Add -m (monitor) to keep listening forever, and -q to silence the “Setting up watches” chatter:
inotifywait -m -q /tmp/demoNow every touch, cat, and rm in that directory prints a line. The raw output is noisy. You will want to pick your events and format the output.
Events: Pick the Right One or Eat Corrupt Files
The -e flag selects events. The list lives in inotifywait --help: access, modify, attrib, close_write, close_nowrite, open, moved_to, moved_from, create, delete, delete_self, move_self, unmount, and a few combined names like close and move.
Beginners reach for create first. It sounds right. It is a trap. Here is a slow upload simulated with a writer that takes a second between two lines:
mkdir d1inotifywait -m -q --format '%e %f' -e create -e modify -e close_write d1 &( echo part1; sleep 1; echo part2 ) > d1/big.txtCREATE big.txtMODIFY big.txtMODIFY big.txtCLOSE_WRITE,CLOSE big.txtcreate fires the moment the file exists, with zero bytes in it. modify fires on every write. If your script reacts to either one, it will happily gzip, upload, or parse half a file, and your 2 AM self will be reading a corrupt-archive error.
close_write fires once, when the writer closes the file. That is your “this file is done” signal. For files that arrive by mv or rsync (which writes to a temp name, then renames), add moved_to. The rule for a watch folder is simple:
-e close_write -e moved_toBoth forms of -e work: repeat the flag or comma-separate (-e close_write,moved_to).
The Watch-Folder Script
Here is the pattern you will reuse. It processes anything that lands in an inbox, moves it out of the way, and survives ugly filenames.
#!/usr/bin/env bashset -uINBOX=${INBOX:-/srv/inbox}DONE=${DONE:-/srv/done}mkdir -p "$INBOX" "$DONE"
process() { local path=$1 [ -f "$path" ] || return 0 # already handled, or a directory printf 'processing: %s (sha256 %s)\n' "$path" "$(sha256sum < "$path" | cut -c1-12)" mv -n -- "$path" "$DONE"/}
# Start the monitor first so nothing is missed during the startup sweep.exec 3< <(inotifywait -m -q -e close_write -e moved_to \ --no-newline --format '%w%f%0' "$INBOX")
# Catch files that were already waiting.find "$INBOX" -maxdepth 1 -type f -print0 | while IFS= read -r -d '' f; do process "$f"; done
while IFS= read -r -d '' path <&3; do process "$path"doneI tested it with a file that was already waiting, a slow two-second upload named slow upload.txt, and a file moved in with mv. All three were processed once, and all three ended up in the done directory.
Four decisions in that script do the real work. First, -m instead of a loop. The lazy version is while inotifywait -e close_write dir; do handle; done. Between the moment inotifywait exits and the moment the next one starts, no watch exists. Files that land in that gap are never reported. A single -m process keeps the watch alive and queues events for you. Events queue up while your handler runs, so a slow handler delays work. It does not drop it, until the kernel queue (fs.inotify.max_queued_events) overflows.
Second, NUL-separated output. The default output splits fields on spaces, so my file.txt becomes two words and a filename with a newline breaks a while read loop outright. --format '%w%f%0' prints the directory plus filename followed by a NUL byte, and --no-newline stops inotifywait from tacking on a newline. read -r -d '' reads NUL-terminated records. I fed it a file named my file.txt and one with a newline in the middle of the name, and both came through whole. If you find NUL records too fussy, -c gives CSV output, which the man page calls useful for names with spaces.
Third, start the monitor, then sweep. If you sweep first and start watching second, anything that arrives between the two steps is missed. Starting the monitor first means a file can show up twice (once in the sweep, once as an event), which is why process checks that the file still exists and moves it away.
Fourth, move finished files out of the inbox. That makes the script idempotent. Restart it, crash it, run it twice: the inbox only contains work that has not been done.
Never write the output into the directory you are watching. A script that reacts to its own output is an infinite loop with extra steps.
Format, Filters, and Time
The --format flag takes printf-style conversions. The ones you need are %w (the watched path), %f (the filename inside it), %e (the events, comma-separated), and %T (a timestamp, shaped by --timefmt):
inotifywait -m -q --timefmt '%H:%M:%S' --format '%T %w %f %e' -e close_write d520:02:48 d5/ a.txt CLOSE_WRITE,CLOSEEditors and tools scatter temp files everywhere (.swp, .tmp, ~). Filter them with --exclude, which takes a POSIX extended regular expression matched against the path:
inotifywait -m -q -e close_write --exclude '\.(tmp|swp)$' --format '%f' d6I created keep.txt and skip.tmp in that directory and only keep.txt printed. --include is the inverse (only paths matching are reported). Both have case-insensitive twins, --excludei and --includei. The help text says only the last --exclude counts, so combine patterns into one with |.
For a one-shot wait inside a script, -t sets a timeout in seconds. The exit codes, straight from the help text: 0 for a wanted event, 1 for an unwanted event or an error, 2 for a timeout. I confirmed the timeout case:
inotifywait -q -t 1 -e close_write d3; echo "exit: $?"exit: 2That makes if inotifywait -q -t 30 -e close_write "$file"; then ... a clean “wait up to 30 seconds for this to finish” check.
Recursion, and the Watch Limit
-r watches a whole tree, including directories created later. Under the hood, inotify needs one watch per directory, and the kernel caps the total per user:
cat /proc/sys/fs/inotify/max_user_watchesThe default depends on your kernel. Since Linux 5.11 it scales with RAM (sized so watches use at most about 1% of memory, clamped between 8192 and 1048576), so older docs quoting a flat 8192 are out of date on a current box. The machine I tested on printed 524288. A small VM or an older kernel can be much lower. Point -r at a photo library with 100,000 folders on a box with a low limit, and inotifywait stops with an error saying the upper limit on inotify watches was reached.
Raise the limit with a sysctl:
echo 'fs.inotify.max_user_watches = 524288' | sudo tee /etc/sysctl.d/90-inotify.confsudo sysctl --systemEach watch costs a little kernel memory, so do not set it to a billion “just in case”. Better: watch the directory you care about, not its parent. Pointing -r at / is the forklift-and-couch move.
The Editor Gotcha: Watch the Directory
You watch config.ini with inotifywait -m config.ini. You edit it in vim. You edit it again. Nothing fires the second time.
Many editors, and sed -i, save by writing a new file and renaming it over the old one. Your watch is attached to the old inode, which now belongs to nobody. I reproduced it with sed -i on a watched file:
OPENMODIFYCLOSE_WRITE,CLOSEOPENACCESSCLOSE_NOWRITE,CLOSEATTRIBDELETE_SELFThe first three lines are a normal in-place write. The rest is sed -i: it reads the file, then the old file is deleted (DELETE_SELF), and the monitor exits because it has nothing left to watch. A write I made afterward went unseen.
Watch the directory instead and filter by name. The same sed -i, with the directory watched:
CLOSE_WRITE,CLOSE sedEX9A10MOVED_TO f.txtThe temp file closes, then gets renamed onto f.txt, which shows up as moved_to. This is why the watch-folder script uses close_write plus moved_to. For a single config file, the pattern is:
#!/usr/bin/env bashdir=/etc/myapp; name=config.iniinotifywait -m -q -e close_write -e moved_to --format '%f' "$dir" |while IFS= read -r f; do [ "$f" = "$name" ] && systemctl reload myappdoneDebounce: One Save Is Never One Event
Save a project in an IDE or run git checkout and you get a burst of events. Reacting to each one runs your rebuild ten times. A debounce waits for quiet, then fires once:
#!/usr/bin/env bash# Usage: ./debounce.sh /srv/site/src 2 ./rebuild.shdir=$1 quiet=$2; shift 2inotifywait -m -r -q -e close_write,moved_to,delete --format '%w%f' "$dir" |while IFS= read -r _first; do # keep draining events until $quiet seconds pass with nothing new while IFS= read -r -t "$quiet" _more; do :; done "$@"doneTen files written 100 ms apart produced exactly one rebuild at ... line in my test with a one-second quiet window. The trick is read -t: the inner loop keeps eating events and returns the moment a full quiet window passes with none.
Pick the quiet window to match your slowest writer. Two seconds suits an IDE. A big rsync may need ten, or better, wait for its moved_to events.
entr: When You Just Want to Rerun a Command
Half the scripts people write with inotifywait are “run the tests when I save”. For that, skip the plumbing. entr reads a list of files on standard input and reruns a command when any of them changes:
ls *.py | entr pytestfind src -name '*.go' | entr -c go test ./...The flags worth knowing, all from man entr in the tested package:
| Flag | What it does |
|---|---|
-r | Restart a long-running process (a server) with SIGTERM on each change |
-c | Clear the screen before each run (twice also clears scrollback) |
-d | Watch the directories of the listed files and exit when a new file appears |
-n | Non-interactive: do not read from the TTY (needed in cron, CI, containers) |
-p | Postpone the first run until a file changes |
-s | Run the first argument through $SHELL, so pipes and && work |
-z | Exit after the command finishes |
/_ | Placeholder for the first file that triggered the run |
Show which file triggered the run:
ls *.sh *.txt | entr -n sh -c 'echo "ran, trigger: $0"' /_ran, trigger: /tmp/tmp.JnAQ6GyDgF/app.shNote that entr runs a command and /_ is substituted into its arguments. Without -s or sh -c there is no shell, so no pipes.
-r is the one that earns its keep for dev servers. I ran a sleep 100 stand-in under entr -r and edited the watched file; the old process died and a new one started with a fresh PID:
server start pid 64server start pid 67The new-file trap
entr takes its file list once, at startup. A file created later is not in the list. With -d it notices the new file, but its answer is to exit with status 2 and the message entr: directory altered. I confirmed both. The fix is a loop that re-runs ls and starts entr again, straight from the man page:
while sleep 0.1; do ls src/*.rb | entr -d make; doneThat loop fakes what inotifywait -m -r does natively, so if you find yourself writing it a lot, reconsider.
Which One Do You Reach For
| You want to… | Use |
|---|---|
| Rerun tests or a build on save | entr |
| Restart a dev server on change | entr -r |
| Process each new file in a folder | inotifywait -m |
| Know which file changed and how | inotifywait --format |
| Watch a whole tree that grows | inotifywait -r |
| Run something once per burst | inotifywait plus a debounce |
My rule: if the command does not care which file changed, entr. If your script branches on the filename or the event type, inotifywait. Either one beats a cron job polling ls for the same answer.
Where inotify Does Not Reach
inotify reports changes made through the local kernel’s filesystem calls. Two places to be careful:
- NFS and SMB: a change made by another machine on a network share does not generate an event on your machine. Local changes through your own mount do. If the files arrive over the network, poll, or run the watcher on the server that owns the disk.
- FUSE and container mounts: support varies by filesystem. Test with a
touchbefore you trust a watch on a FUSE mount. Docker bind mounts on Linux are plain local paths and work. On Docker Desktop (macOS, Windows), host edits pass through a virtualization layer, so test it there before relying on events.
I did not test any of the network or desktop cases above. The container tests ran on a local filesystem only.
The SumGuy Take
Use entr for the 80% case, because one line beats twenty. Use inotifywait -m with close_write,moved_to for anything that handles files, and always watch the directory, never the file. Pipe it through NUL-separated output so a weird filename does not become your 2 AM page. Add a debounce when saves come in bursts.
And delete the cron job that polls every minute. It had a good run.
Common Questions
Does inotifywait work on NFS mounts?
No, not for changes made by other machines. The kernel generates inotify events only for filesystem calls made on the local machine. Writes through your own NFS mount do trigger events, but a change from another client does not. Run the watcher on the NFS server, or poll instead.
How do I stop inotifywait from missing events between runs?
Run inotifywait -m once instead of calling inotifywait in a loop. Without -m, the command exits after one event, and files that arrive before the next call starts are never reported. With -m, one process keeps the watch open and the events queue in the pipe.
Should I use close_write or modify to detect a finished file?
Use close_write. The modify event fires on every write, so a large upload triggers it many times while the file is still incomplete. The close_write event fires once after the writer closes the file. Add moved_to to catch files that arrive by mv or rsync renames.
What is the difference between entr and inotifywait?
The entr tool reruns one command whenever any listed file changes, and it does not tell the command what happened. The inotifywait tool prints each event with the path and event type, so a script can branch on them. Choose entr for rebuilds, inotifywait for file-handling scripts.
Does entr work on macOS?
Yes. The entr project supports BSD and macOS as well as Linux, so the same one-liners run on a Mac. The inotifywait command does not, because inotify is a Linux kernel feature. On macOS, use entr or a native watcher built on FSEvents.