The Problem: You Can’t Watch Every File Manually
Picture this: a deployment script drops a config file into /etc/app/, but the service doesn’t reload. Or a user uploads a file to /var/uploads/ and nothing happens until someone manually kicks off a processing job. You end up either polling with a cron job every minute — wasteful and laggy — or writing complex daemon code from scratch.
After managing 10+ Linux VPS instances over three years, I found that reactive automation — acting the moment a file changes — is far more reliable than scheduled polling. The Linux kernel already tracks every filesystem event. You just need the right tools to tap into it.
Enter inotify, inotifywait, and incron. React to file events instantly, no daemon code required.
Core Concepts: How Linux Tracks File Events
The inotify Kernel Subsystem
Introduced in Linux kernel 2.6.13, inotify (inode notify) lets processes watch files and directories for specific events — no polling, no timers. The kernel pushes notifications the instant something changes.
Supported events include:
IN_CREATE— a file or directory was createdIN_MODIFY— a file was written toIN_DELETE— a file or directory was deletedIN_MOVED_FROM/IN_MOVED_TO— file was renamed or movedIN_CLOSE_WRITE— a writable file was closed (very useful for detecting completed uploads)IN_ATTRIB— metadata changed (permissions, ownership, timestamps)
inotifywait: Command-Line Access to inotify
inotifywait ships as part of the inotify-tools package. It blocks until an event occurs, prints the event, then exits — or keeps looping with the -m flag. Wrap it in a while loop and you have a simple, scriptable file watcher.
incron: inotify + cron = incron
Think of incron as cron, but triggered by filesystem events rather than time. Define rules in an incrontab file, and incrond runs your command automatically when the event fires. No shell loop, no manual restarts — it runs as a daemon in the background.
Hands-On Practice
Step 1: Install the Tools
On Debian/Ubuntu:
sudo apt update
sudo apt install inotify-tools incron
On RHEL/AlmaLinux/Rocky:
sudo dnf install inotify-tools incron
Step 2: Quick Test with inotifywait
Before wiring up any automation, always verify events manually. Open two terminals.
Terminal 1 — start watching:
inotifywait -m -r -e create,modify,delete /tmp/watch-test/
Terminal 2 — trigger some events:
mkdir -p /tmp/watch-test
touch /tmp/watch-test/hello.txt
echo "data" >> /tmp/watch-test/hello.txt
rm /tmp/watch-test/hello.txt
You’ll see output like:
/tmp/watch-test/ CREATE hello.txt
/tmp/watch-test/ MODIFY hello.txt
/tmp/watch-test/ DELETE hello.txt
This confirms the kernel is reporting events correctly for that directory. I’ve been burned before by a symlink pointing somewhere inotify wasn’t actually watching — this quick test catches that before anything breaks in production.
Step 3: Write a Shell Loop Watcher with inotifywait
For lightweight, one-off automation, a shell script is the fastest path forward:
#!/bin/bash
# watch-uploads.sh — process files as soon as they arrive
WATCH_DIR="/var/uploads"
PROCESS_SCRIPT="/opt/scripts/process-file.sh"
inotifywait -m -e close_write --format '%f' "$WATCH_DIR" | while read FILENAME; do
echo "[$(date)] Detected: $FILENAME"
"$PROCESS_SCRIPT" "$WATCH_DIR/$FILENAME"
done
Key tip: use close_write instead of modify for uploaded files. The modify event fires on every write syscall — copy a 500MB file and you’ll get hundreds of events mid-transfer. close_write fires exactly once, when the file handle closes after writing. Much cleaner.
Run this script as a systemd service so it survives reboots:
# /etc/systemd/system/watch-uploads.service
[Unit]
Description=Watch /var/uploads for new files
After=network.target
[Service]
ExecStart=/opt/scripts/watch-uploads.sh
Restart=on-failure
User=www-data
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now watch-uploads
Step 4: Set Up incron for System-Level File Triggers
incron shines for persistent, daemon-managed rules — config reloads, log rotation triggers, or multi-user setups where each user manages their own incrontab.
First, allow your user (or root) to use incron:
echo "root" | sudo tee -a /etc/incron.allow
Edit the incrontab for root:
sudo incrontab -e
Syntax: <path> <event(s)> <command>
# Reload nginx when config changes
/etc/nginx/conf.d IN_CLOSE_WRITE systemctl reload nginx
# Compress any new log file dropped into /var/log/archive/
/var/log/archive IN_CREATE gzip $@/$#
# Alert when a file is deleted from a sensitive directory
/etc/cron.d IN_DELETE /opt/scripts/alert-cron-delete.sh $#
Special incron variables:
$@— the watched directory path$#— the filename that triggered the event$$— a literal$sign$%— the event flags as a string
Enable and start incrond:
sudo systemctl enable --now incrond
sudo systemctl status incrond
Step 5: Watching Recursively and Filtering Events
One common gotcha: incron does not watch subdirectories by default. For recursive watching, inotifywait -r is the better choice. With incron, you either add a rule per subdirectory or write a wrapper that dynamically registers new watches.
Filtering by filename pattern in inotifywait is easy with grep:
inotifywait -m -e close_write --format '%f' /var/uploads | \
grep --line-buffered '\.csv$' | \
while read FILENAME; do
/opt/scripts/import-csv.sh "/var/uploads/$FILENAME"
done
Don’t forget --line-buffered on grep. Without it, grep buffers output in chunks and your script appears to hang waiting. This trips up almost everyone the first time they pipe inotifywait output through grep.
Step 6: Debouncing Rapid Events
Some editors and tools write files in multiple steps — write a temp file, rename it, delete the original. A single logical save generates a burst of events. Here’s a simple debounce in bash:
#!/bin/bash
WATCH_DIR="/etc/app"
DEBOUNCE=2 # seconds to wait after last event
inotifywait -m -e close_write,moved_to --format '%f' "$WATCH_DIR" | \
while read FILENAME; do
# Cancel pending reload if one is queued
[ -n "$DEBOUNCE_PID" ] && kill "$DEBOUNCE_PID" 2>/dev/null
(
sleep $DEBOUNCE
echo "[$(date)] Reloading due to change: $FILENAME"
systemctl reload myapp
) &
DEBOUNCE_PID=$!
done
This waits 2 seconds after the last event before triggering the action. During a rolling deployment that rewrites a config file 4–5 times in quick succession, you get exactly one reload instead of five.
Practical Tip: Check Your inotify Watch Limits
Each inotify watch consumes a kernel resource. The default limit is 8,192 watches per user. On a server with many watched directories, you can hit that ceiling fast:
# Check current limit
cat /proc/sys/fs/inotify/max_user_watches
# Increase it temporarily
sudo sysctl fs.inotify.max_user_watches=524288
# Make it permanent
echo "fs.inotify.max_user_watches=524288" | sudo tee -a /etc/sysctl.d/99-inotify.conf
sudo sysctl -p /etc/sysctl.d/99-inotify.conf
This limit is easy to underestimate. On one VPS running both a Node.js app (which uses inotify heavily for hot-reloading) and our incron rules, we silently hit the ceiling. Incron stopped triggering with zero error output — just silence. Running dmesg | grep inotify revealed the culprit immediately. Always check this before deploying to production.
When to Use Which Tool
- inotifywait in a loop: Quick scripts, development automation, single-purpose watchers. Easy to test interactively.
- incron: System-level rules you want managed as a daemon. Ideal for ops tasks like auto-reloading services or compressing logs. All rules live in one auditable incrontab.
- Python’s watchdog library: Cross-platform file watching inside a Python application. It wraps inotify with a clean API and handles platform differences transparently.
Putting inotify to Work
Switching from cron-every-minute polling to event-driven file watching cuts latency from up to 60 seconds down to milliseconds. It also eliminates unnecessary CPU wake-ups — the process sleeps until the kernel signals it.
Start small. Pick one directory, watch it with inotifywait -m for a day, and observe the events. You’ll spot automation opportunities fast — config files that need reloading, uploads waiting on a processing job, sensitive paths that should never change silently.
Between inotifywait for scripting and incron for daemon-managed rules, you can cover 90% of real-world file monitoring needs without writing a single line of C or deploying a heavyweight monitoring agent.

