inotifywait - Filesystem Event Watcher
inotifywait listens for filesystem events in real time on Linux. Instead of polling every N seconds, it blocks until the kernel reports an event such as create, modify, move, delete, or close-write. This makes it fast and efficient for automation pipelines: ingest folders, deployment triggers, log watchers, media processors, and sync jobs.
inotifywait is event-driven, so it reacts immediately when a watched path changes. It is powerful, but it has sharp edges: event storms, duplicate events, kernel watch limits, and script race conditions. This page covers practical, production-safe usage end-to-end.
System Check
command -v inotifywait || echo "inotifywait is not installed"
inotifywait --version
# Current inotify kernel limits
cat /proc/sys/fs/inotify/max_user_watches
cat /proc/sys/fs/inotify/max_user_instances
cat /proc/sys/fs/inotify/max_queued_events
Install inotifywait
inotifywait comes from the inotify-tools package.
# Debian/Ubuntu
sudo apt update && sudo apt install -y inotify-tools
# RHEL/CentOS/Alma/Rocky
sudo dnf install -y inotify-tools
# Arch Linux
sudo pacman -S --noconfirm inotify-tools
Verify installation:
command -v inotifywait
inotifywait --version
When to Use inotifywait
| Scenario | inotifywait fit | Why |
|---|---|---|
| Trigger on file arrival/change | strong | immediate event-driven reaction |
| Build ingest pipeline from directory drops | strong | low-latency and low overhead |
| Need exact time schedules | weak | use cron or systemd timers |
| Need portable non-Linux behavior | weak | inotify is Linux-specific |
| Need strict service-level controls and dependencies | medium | wrap with systemd service for production |
Quick rule:
- choose inotifywait for event-based triggers,
- choose cron/anacron/systemd timers for time-based scheduling,
- choose systemd.path when you want event triggers integrated with systemd units.
Benefits
- Immediate reaction - jobs start as soon as filesystem events occur.
- Low CPU usage - no tight polling loop needed.
- Fine-grained filtering - watch only selected events and file patterns.
- Script-friendly output - format lines for shell pipelines.
- Good for automation glue - simple to combine with rsync, compressors, converters, and queue workers.
- Linux kernel backed - reliable local filesystem event source.
Best Practices
- Use
close_writefor "file is ready" workflows - avoid processing partially written files. - Filter aggressively - limit events and file patterns with
-e,--include,--exclude. - Use absolute paths in automation - avoid cwd confusion in services/cron.
- Debounce noisy paths - bursts can trigger too many downstream jobs.
- Add locking around heavy tasks - prevent overlap with
flock. - Handle rename/move semantics - many applications write temp files then rename.
- Tune inotify limits for recursive trees - deep trees can exhaust watch counts.
- Log raw events during debugging - then narrow to production filters.
- Use systemd for long-running watchers - restart behavior and logs are better than ad-hoc background shells.
- Keep watcher and processor separated - watcher emits events, processor script handles validation/retry.
Tips & Strategy
close_write over modify for pipelinesmodify fires repeatedly while a file is being written. close_write usually maps better to "safe to process now".
Use %w%f and pass full path to scripts; avoid fragile parsing of just filenames.
Confirm file type/size/path before acting, especially for delete/move events.
One watch per directory can consume limits quickly on large trees.
You get restart policy, journald logs, and clean lifecycle management.
How inotifywait Works
inotifywait subscribes to kernel inotify events for watched files/directories. When an event matches your filters, it prints a line and exits (default) or continues (--monitor).
Operational model:
- Register watch(es).
- Block waiting for events.
- Receive event from kernel queue.
- Print event using default or custom format.
- Exit or continue based on mode.
By default (no -m), the process exits after one matching event.
Core Syntax
inotifywait [options] path1 [path2 ...]
Most-used options
| Option | Meaning | Typical use |
|---|---|---|
-m, --monitor | keep listening continuously | long-running watcher |
-r, --recursive | watch directories recursively | trees/uploads roots |
-e, --event | select specific event(s) | reduce noise |
-t, --timeout | timeout in seconds | tests, one-shot waits |
--format | custom output format | scripting/parsing |
--timefmt | timestamp format for %T | logging |
-c, --csv | CSV output | machine parsing |
--include | include regex pattern | process only matching files |
--exclude | exclude regex pattern | skip temp/cache files |
--fromfile | read watch paths from file/stdin | large watch sets |
-o, --outfile | write output to file | daemon mode logging |
-d, --daemon | run background monitor | detached workflows |
Event Reference
Common events supported by inotifywait:
| Event | Meaning | Common use |
|---|---|---|
create | entry created in watched dir | new file arrival |
modify | file content changed | live edits/log tails |
close_write | writable file handle closed | process completed upload |
delete | entry removed from watched dir | cleanup triggers |
moved_to | entry moved into watched dir | atomic delivery patterns |
moved_from | entry moved out | pipeline completion/cleanup |
move | moved in or out | generalized move logic |
attrib | metadata changed | chmod/chown/timestamp checks |
delete_self | watched file/dir deleted | watcher lifecycle handling |
unmount | backing fs unmounted | stop/restart watcher actions |
open | file opened | audit-style flows |
access | file read | read-monitoring/debug |
Use small event sets whenever possible:
inotifywait -m -e close_write -e moved_to /srv/incoming
Output Formatting for Automation
Default output is human-friendly but not always script-friendly. Prefer explicit format.
Example format string:
inotifywait -m -e close_write --format '%T,%w%f,%e' --timefmt '%Y-%m-%dT%H:%M:%S%z' /srv/incoming
Useful placeholders:
| Token | Meaning |
|---|---|
%w | watched path |
%f | filename within watched dir |
%e | event list |
%T | timestamp (with --timefmt) |
CSV output option:
inotifywait -m -c -e create -e close_write /srv/incoming
Include/Exclude Filtering
Use include/exclude patterns to keep event volume manageable.
inotifywait -m -r -e close_write --include '\\.(jpg|jpeg|png|webp)$' /srv/incoming
inotifywait -m -r -e modify --exclude '(\\.swp$|\\.tmp$|/\\.git/|/node_modules/)' /srv/project
Notes:
- Patterns are extended regular expressions.
--includekeeps only matching paths/events.--excludedrops matching paths/events.
Recursive Watches and Kernel Limits
Recursive mode can consume many watches because directories need individual watch entries.
Inspect limits:
cat /proc/sys/fs/inotify/max_user_watches
cat /proc/sys/fs/inotify/max_user_instances
cat /proc/sys/fs/inotify/max_queued_events
Temporary tuning:
sudo sysctl -w fs.inotify.max_user_watches=524288
sudo sysctl -w fs.inotify.max_user_instances=1024
sudo sysctl -w fs.inotify.max_queued_events=65536
Persistent tuning:
sudo tee /etc/sysctl.d/99-inotify.conf >/dev/null <<'EOF'
fs.inotify.max_user_watches=524288
fs.inotify.max_user_instances=1024
fs.inotify.max_queued_events=65536
EOF
sudo sysctl --system
If event throughput exceeds queue capacity, events can be dropped. Monitor logs and keep event handlers fast.
Exit Codes and Timeouts
inotifywait uses useful exit codes:
| Exit code | Meaning |
|---|---|
0 | requested event received |
1 | unexpected event or error |
2 | timeout occurred (-t) |
Timeout example:
inotifywait -t 30 -e create /srv/incoming
echo "exit=$?"
Practical Examples (25+)
1. Wait for one create event
inotifywait -e create /srv/incoming
2. Monitor continuously
inotifywait -m -e create -e close_write /srv/incoming
3. Monitor recursively
inotifywait -m -r -e close_write /srv/incoming
4. Watch only "file complete" events
inotifywait -m -e close_write /srv/incoming
5. Watch move-in events (atomic delivery)
inotifywait -m -e moved_to /srv/incoming
6. Watch delete events
inotifywait -m -e delete -e delete_self /srv/incoming
7. Add timestamped structured output
inotifywait -m -e close_write --format '%T|%w%f|%e' --timefmt '%Y-%m-%d %H:%M:%S' /srv/incoming
8. CSV output for ingestion
inotifywait -m -c -e create -e close_write /srv/incoming
9. Include only PDFs
inotifywait -m -e close_write --include '\\.(pdf)$' /srv/incoming
10. Include multiple document types
inotifywait -m -e close_write --include '\\.(pdf|docx|xlsx|pptx)$' /srv/incoming
11. Exclude temp/swap files
inotifywait -m -r -e modify --exclude '(\\.swp$|\\.tmp$|~$)' /srv/project
12. Timeout after 60 seconds
inotifywait -t 60 -e close_write /srv/incoming
echo "exit=$?"
13. Read watch paths from file
inotifywait -m --fromfile /etc/inotify/watch-paths.txt -e close_write
14. Read watch paths from stdin
printf '%s\n' /srv/incoming /srv/queue | inotifywait -m --fromfile - -e create
15. Trigger rsync on completed file writes
inotifywait -m -e close_write --format '%w%f' /srv/incoming | while read -r file; do
rsync -az "$file" backup:/srv/incoming/
done
16. Trigger image optimization pipeline
inotifywait -m -e close_write --include '\\.(jpg|jpeg|png)$' --format '%w%f' /srv/images | while read -r file; do
/usr/local/bin/optimize-image "$file"
done
17. Auto-reload nginx when config changes
inotifywait -m -e close_write /etc/nginx/nginx.conf /etc/nginx/conf.d | while read -r _; do
nginx -t && systemctl reload nginx
done
18. Watch WordPress uploads
inotifywait -m -r -e moved_to -e close_write --format '%w%f %e' /var/www/html/wp-content/uploads
19. Process queue directory only when files arrive
inotifywait -m -e create -e moved_to --format '%w%f' /srv/queue | while read -r file; do
/usr/local/bin/process-queue-item "$file"
done
20. Run with lock to prevent overlap
inotifywait -m -e close_write --format '%w%f' /srv/incoming | while read -r file; do
flock -n /var/lock/process-file.lock /usr/local/bin/process-file "$file"
done
21. Debounce bursty changes (simple)
inotifywait -m -e modify --format '%w%f' /srv/project | while read -r file; do
sleep 1
/usr/local/bin/rebuild-if-needed "$file"
done
22. Daemon mode output to log file
inotifywait -d -m -r -e close_write -o /var/log/inotify-events.log /srv/incoming
23. Quiet monitor for scripting only
inotifywait -m -q -e close_write --format '%w%f' /srv/incoming
24. Watch unmount and self-delete for lifecycle handling
inotifywait -m -e unmount -e delete_self /srv/incoming
25. Trigger cert deploy hook after cert file write
inotifywait -m -e close_write --include 'fullchain\\.pem$' --format '%w%f' /etc/letsencrypt/live | while read -r _; do
systemctl reload nginx
done
26. Watch multiple roots in one command
inotifywait -m -e create -e moved_to /srv/incoming /srv/queue /srv/dropbox
27. One-shot trigger for deployment artifact
inotifywait -t 3600 -e moved_to --include 'release\\.tar\\.gz$' /srv/releases
28. Watch only non-symlink paths
inotifywait -m -P -e close_write /srv/incoming
29. Watch metadata changes only
inotifywait -m -e attrib /srv/config
30. Capture and parse event stream safely
inotifywait -m -e close_write --format '%w%f|%e' /srv/incoming | while IFS='|' read -r path event; do
/usr/local/bin/handle-event "$path" "$event"
done
Safe Watcher Script Template
Use a dedicated script for production watchers.
#!/usr/bin/env bash
set -euo pipefail
WATCH_DIR="/srv/incoming"
LOCK="/var/lock/incoming-processor.lock"
LOG="/var/log/incoming-processor.log"
mkdir -p "$(dirname "$LOG")"
inotifywait -m -r -e close_write -e moved_to --format '%T|%w%f|%e' --timefmt '%Y-%m-%dT%H:%M:%S%z' "$WATCH_DIR" |
while IFS='|' read -r ts path event; do
{
echo "[$ts] event=$event path=$path"
# Basic safety checks
[ -f "$path" ] || { echo "skip: not a regular file"; continue; }
# Avoid overlap in processor
flock -n "$LOCK" /usr/local/bin/process-incoming "$path"
} >> "$LOG" 2>&1
done
Run Watchers with systemd (Recommended)
Long-running watcher loops should run as a managed service.
[Unit]
Description=Incoming directory watcher
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/incoming-watcher.sh
Restart=always
RestartSec=3
User=root
Group=root
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now incoming-watcher.service
systemctl --no-pager --full status incoming-watcher.service
journalctl -u incoming-watcher.service --since '1 hour ago' --no-pager
Debugging inotifywait Pipelines
Step-by-step workflow
- Start raw monitor with broad events to confirm events arrive.
- Generate a known test event (touch/mv/cp).
- Narrow events and filters to production set.
- Validate parser loop with quoted paths and weird filenames.
- Check kernel limits when using recursive mode.
- Measure handler runtime to avoid event queue backlogs.
# 1) Raw watch
inotifywait -m -r /srv/incoming
# 2) Focused watch
inotifywait -m -r -e close_write -e moved_to --format '%w%f %e' /srv/incoming
# 3) Limit checks
cat /proc/sys/fs/inotify/max_user_watches
cat /proc/sys/fs/inotify/max_queued_events
# 4) If managed by systemd
systemctl --no-pager --full status incoming-watcher.service || true
journalctl -u incoming-watcher.service --since '2 hours ago' --no-pager || true
Common issues
| Symptom | Likely cause | Fix |
|---|---|---|
| No events received | wrong path or missing permissions | verify watched path and user permissions |
| Too many triggers per file | using modify for large writes | switch to close_write and add debounce |
| Missed events under heavy load | queue overflow or slow handler | raise limits, speed up handler, decouple processing |
| "No space left on device" on watch setup | max watches reached | increase fs.inotify.max_user_watches |
| Processing partial files | acting on create too early | trigger on close_write or moved_to |
| Script breaks on spaces | unsafe parsing | use read -r, quote variables, use custom delimiters |
Hands-On Practice
Quick Lab (5 Exercises)
- Create
/tmp/inotify-laband runinotifywait -m -e create -e close_write /tmp/inotify-lab. - In another shell, create and edit files to observe event differences.
- Switch to
--format '%T|%w%f|%e' --timefmt '%H:%M:%S'and record output. - Add
--include '\\.(txt|log)$'and confirm filtering behavior. - Convert monitor into a script that appends events to
/tmp/inotify-lab/events.log.
Task: Build an Upload Processor
Build a watcher that:
- monitors
/srv/uploadsrecursively, - reacts only to completed upload events,
- processes only image files,
- prevents overlapping processor runs,
- logs timestamp, path, and event.
Solution
#!/usr/bin/env bash
set -euo pipefail
WATCH_DIR="/srv/uploads"
LOCK="/var/lock/upload-processor.lock"
LOG="/var/log/upload-processor.log"
inotifywait -m -r -e close_write -e moved_to --include '\\.(jpg|jpeg|png|webp)$' \
--format '%T|%w%f|%e' --timefmt '%Y-%m-%dT%H:%M:%S%z' "$WATCH_DIR" |
while IFS='|' read -r ts path event; do
{
echo "[$ts] event=$event path=$path"
[ -f "$path" ] || exit 0
flock -n "$LOCK" /usr/local/bin/process-image "$path"
} >> "$LOG" 2>&1
done
Mini Quiz
- Why is
close_writeoften safer thanmodifyfor file-processing pipelines? - What does
-mchange ininotifywaitbehavior? - Which option lets you monitor directories recursively?
- How do you print custom parseable output lines?
- Which sysctl controls the total watch count per user?
- What is exit code
2used for? - How do
--includeand--excludereduce event noise? - Why should long-running watchers be run under systemd?
- What problem does
flocksolve in event handlers? - When should you prefer
systemd.pathover a shell watcher loop?
Cheat Sheet
# Basic
inotifywait -e create /path
inotifywait -m -e close_write /path
inotifywait -m -r -e close_write /path
# Filters
inotifywait -m -e close_write --include '\\.(jpg|png)$' /path
inotifywait -m -e modify --exclude '(\\.swp$|\\.tmp$)' /path
# Format
inotifywait -m -e close_write --format '%T|%w%f|%e' --timefmt '%F %T' /path
inotifywait -m -c -e create /path
# Timeout and exit code
inotifywait -t 30 -e moved_to /path; echo $?
# Kernel limits
cat /proc/sys/fs/inotify/max_user_watches
cat /proc/sys/fs/inotify/max_user_instances
cat /proc/sys/fs/inotify/max_queued_events
# Temporary tuning
sudo sysctl -w fs.inotify.max_user_watches=524288
sudo sysctl -w fs.inotify.max_user_instances=1024
sudo sysctl -w fs.inotify.max_queued_events=65536
# Completed write trigger
inotifywait -m -e close_write --format '%w%f' /srv/incoming
# Atomic move-in trigger
inotifywait -m -e moved_to --format '%w%f' /srv/incoming
# Combined safe trigger for upload pipelines
inotifywait -m -e close_write -e moved_to --format '%w%f %e' /srv/incoming