Skip to main content

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.

install-inotify-tools-by-distro.sh
# 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:

verify-inotifywait-install.sh
command -v inotifywait
inotifywait --version

When to Use inotifywait

Scenarioinotifywait fitWhy
Trigger on file arrival/changestrongimmediate event-driven reaction
Build ingest pipeline from directory dropsstronglow-latency and low overhead
Need exact time schedulesweakuse cron or systemd timers
Need portable non-Linux behaviorweakinotify is Linux-specific
Need strict service-level controls and dependenciesmediumwrap 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_write for "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

Prefer close_write over modify for pipelines

modify fires repeatedly while a file is being written. close_write usually maps better to "safe to process now".

Process by path, not by event text alone

Use %w%f and pass full path to scripts; avoid fragile parsing of just filenames.

Include safety checks in processor scripts

Confirm file type/size/path before acting, especially for delete/move events.

Treat recursive watches as capacity planning

One watch per directory can consume limits quickly on large trees.

For production daemons, use systemd service units

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:

  1. Register watch(es).
  2. Block waiting for events.
  3. Receive event from kernel queue.
  4. Print event using default or custom format.
  5. 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

OptionMeaningTypical use
-m, --monitorkeep listening continuouslylong-running watcher
-r, --recursivewatch directories recursivelytrees/uploads roots
-e, --eventselect specific event(s)reduce noise
-t, --timeouttimeout in secondstests, one-shot waits
--formatcustom output formatscripting/parsing
--timefmttimestamp format for %Tlogging
-c, --csvCSV outputmachine parsing
--includeinclude regex patternprocess only matching files
--excludeexclude regex patternskip temp/cache files
--fromfileread watch paths from file/stdinlarge watch sets
-o, --outfilewrite output to filedaemon mode logging
-d, --daemonrun background monitordetached workflows

Event Reference

Common events supported by inotifywait:

EventMeaningCommon use
createentry created in watched dirnew file arrival
modifyfile content changedlive edits/log tails
close_writewritable file handle closedprocess completed upload
deleteentry removed from watched dircleanup triggers
moved_toentry moved into watched diratomic delivery patterns
moved_fromentry moved outpipeline completion/cleanup
movemoved in or outgeneralized move logic
attribmetadata changedchmod/chown/timestamp checks
delete_selfwatched file/dir deletedwatcher lifecycle handling
unmountbacking fs unmountedstop/restart watcher actions
openfile openedaudit-style flows
accessfile readread-monitoring/debug

Use small event sets whenever possible:

watch-only-close-write-and-move.sh
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:

format-output-example.sh
inotifywait -m -e close_write --format '%T,%w%f,%e' --timefmt '%Y-%m-%dT%H:%M:%S%z' /srv/incoming

Useful placeholders:

TokenMeaning
%wwatched path
%ffilename within watched dir
%eevent list
%Ttimestamp (with --timefmt)

CSV output option:

csv-output-example.sh
inotifywait -m -c -e create -e close_write /srv/incoming

Include/Exclude Filtering

Use include/exclude patterns to keep event volume manageable.

include-only-images.sh
inotifywait -m -r -e close_write --include '\\.(jpg|jpeg|png|webp)$' /srv/incoming
exclude-temp-and-swap.sh
inotifywait -m -r -e modify --exclude '(\\.swp$|\\.tmp$|/\\.git/|/node_modules/)' /srv/project

Notes:

  • Patterns are extended regular expressions.
  • --include keeps only matching paths/events.
  • --exclude drops matching paths/events.

Recursive Watches and Kernel Limits

Recursive mode can consume many watches because directories need individual watch entries.

Inspect limits:

show-inotify-limits.sh
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:

increase-inotify-limits-temporarily.sh
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:

increase-inotify-limits-persistently.sh
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
Capacity warning

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 codeMeaning
0requested event received
1unexpected event or error
2timeout occurred (-t)

Timeout example:

wait-with-timeout-example.sh
inotifywait -t 30 -e create /srv/incoming
echo "exit=$?"

Practical Examples (25+)

1. Wait for one create event

inotifywait-single-create.sh
inotifywait -e create /srv/incoming

2. Monitor continuously

inotifywait-monitor-basic.sh
inotifywait -m -e create -e close_write /srv/incoming

3. Monitor recursively

inotifywait-monitor-recursive.sh
inotifywait -m -r -e close_write /srv/incoming

4. Watch only "file complete" events

inotifywait-close-write-only.sh
inotifywait -m -e close_write /srv/incoming

5. Watch move-in events (atomic delivery)

inotifywait-moved-to.sh
inotifywait -m -e moved_to /srv/incoming

6. Watch delete events

inotifywait-delete-events.sh
inotifywait -m -e delete -e delete_self /srv/incoming

7. Add timestamped structured output

inotifywait-structured-output.sh
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-csv-output.sh
inotifywait -m -c -e create -e close_write /srv/incoming

9. Include only PDFs

inotifywait-include-pdf.sh
inotifywait -m -e close_write --include '\\.(pdf)$' /srv/incoming

10. Include multiple document types

inotifywait-include-doc-types.sh
inotifywait -m -e close_write --include '\\.(pdf|docx|xlsx|pptx)$' /srv/incoming

11. Exclude temp/swap files

inotifywait-exclude-temp.sh
inotifywait -m -r -e modify --exclude '(\\.swp$|\\.tmp$|~$)' /srv/project

12. Timeout after 60 seconds

inotifywait-timeout-60s.sh
inotifywait -t 60 -e close_write /srv/incoming
echo "exit=$?"

13. Read watch paths from file

inotifywait-fromfile.sh
inotifywait -m --fromfile /etc/inotify/watch-paths.txt -e close_write

14. Read watch paths from stdin

inotifywait-from-stdin.sh
printf '%s\n' /srv/incoming /srv/queue | inotifywait -m --fromfile - -e create

15. Trigger rsync on completed file writes

inotifywait-trigger-rsync.sh
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-image-optimize.sh
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-nginx-reload.sh
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-wordpress-uploads.sh
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-queue-worker.sh
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-with-flock.sh
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-simple-debounce.sh
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-daemon-mode.sh
inotifywait -d -m -r -e close_write -o /var/log/inotify-events.log /srv/incoming

23. Quiet monitor for scripting only

inotifywait-quiet-monitor.sh
inotifywait -m -q -e close_write --format '%w%f' /srv/incoming

24. Watch unmount and self-delete for lifecycle handling

inotifywait-lifecycle-events.sh
inotifywait -m -e unmount -e delete_self /srv/incoming

25. Trigger cert deploy hook after cert file write

inotifywait-cert-deploy-hook.sh
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-multiple-roots.sh
inotifywait -m -e create -e moved_to /srv/incoming /srv/queue /srv/dropbox

27. One-shot trigger for deployment artifact

inotifywait-deploy-artifact-once.sh
inotifywait -t 3600 -e moved_to --include 'release\\.tar\\.gz$' /srv/releases
inotifywait-no-dereference.sh
inotifywait -m -P -e close_write /srv/incoming

29. Watch metadata changes only

inotifywait-attrib-only.sh
inotifywait -m -e attrib /srv/config

30. Capture and parse event stream safely

inotifywait-safe-parse-loop.sh
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.

safe-inotifywait-watcher-template.sh
#!/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

Long-running watcher loops should run as a managed service.

/etc/systemd/system/incoming-watcher.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
enable-incoming-watcher-service.sh
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

  1. Start raw monitor with broad events to confirm events arrive.
  2. Generate a known test event (touch/mv/cp).
  3. Narrow events and filters to production set.
  4. Validate parser loop with quoted paths and weird filenames.
  5. Check kernel limits when using recursive mode.
  6. Measure handler runtime to avoid event queue backlogs.
debug-inotifywait-workflow.sh
# 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

SymptomLikely causeFix
No events receivedwrong path or missing permissionsverify watched path and user permissions
Too many triggers per fileusing modify for large writesswitch to close_write and add debounce
Missed events under heavy loadqueue overflow or slow handlerraise limits, speed up handler, decouple processing
"No space left on device" on watch setupmax watches reachedincrease fs.inotify.max_user_watches
Processing partial filesacting on create too earlytrigger on close_write or moved_to
Script breaks on spacesunsafe parsinguse read -r, quote variables, use custom delimiters

Hands-On Practice

Quick Lab (5 Exercises)

  1. Create /tmp/inotify-lab and run inotifywait -m -e create -e close_write /tmp/inotify-lab.
  2. In another shell, create and edit files to observe event differences.
  3. Switch to --format '%T|%w%f|%e' --timefmt '%H:%M:%S' and record output.
  4. Add --include '\\.(txt|log)$' and confirm filtering behavior.
  5. 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/uploads recursively,
  • reacts only to completed upload events,
  • processes only image files,
  • prevents overlapping processor runs,
  • logs timestamp, path, and event.
Solution
upload-processor-solution.sh
#!/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

  1. Why is close_write often safer than modify for file-processing pipelines?
  2. What does -m change in inotifywait behavior?
  3. Which option lets you monitor directories recursively?
  4. How do you print custom parseable output lines?
  5. Which sysctl controls the total watch count per user?
  6. What is exit code 2 used for?
  7. How do --include and --exclude reduce event noise?
  8. Why should long-running watchers be run under systemd?
  9. What problem does flock solve in event handlers?
  10. When should you prefer systemd.path over a shell watcher loop?

Cheat Sheet
inotifywait-cheat-sheet.sh
# 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
event-focused-patterns.txt
# 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

What's Next