File Monitoring
Change Detection
Real-time Updates
File Watching
Event Tracking

How do I watch a file for changes?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Many development workflows depend on reacting to file changes the moment they happen. Hot-reloading a web application, tailing a log file, or triggering a build when source code is saved all require a reliable way to watch the filesystem. The good news is that every major operating system exposes kernel-level file notification APIs, and mature libraries wrap them for Python, Node.js, and other languages.

This article explains the OS-level mechanisms first, then shows practical code for the two most popular cross-platform libraries: Python's watchdog and Node.js's chokidar.

OS-Level File Notification APIs

Linux -- inotify

Linux provides the inotify subsystem, which lets a process subscribe to events (create, modify, delete, move) on specific files or directories. The command-line tool inotifywait makes it easy to experiment:

bash
# Block until a change happens in the current directory, then print the event
inotifywait -m -r -e modify,create,delete .

inotify is efficient because the kernel pushes events to your process rather than forcing it to poll. One limitation is that each watch consumes a file descriptor, so monitoring very large directory trees may require raising fs.inotify.max_user_watches.

macOS -- FSEvents

macOS uses the FSEvents framework, which monitors entire directory trees at once rather than individual files. FSEvents is coarser-grained than inotify: it tells you that something changed inside a directory, and your application must determine what exactly changed. The command-line tool fswatch abstracts over both FSEvents and inotify:

bash
fswatch -o /path/to/directory

Windows -- ReadDirectoryChangesW

Windows exposes ReadDirectoryChangesW, a Win32 API that delivers change notifications for a directory and optionally its subtree. Most cross-platform libraries wrap this API on Windows behind the same interface used for inotify and FSEvents.

Python -- watchdog

The watchdog library provides a cross-platform Python API that uses the best available backend on each OS (inotify on Linux, FSEvents on macOS, ReadDirectoryChangesW on Windows). Install it with pip install watchdog.

python
1import time
2from watchdog.observers import Observer
3from watchdog.events import FileSystemEventHandler
4
5class ChangeHandler(FileSystemEventHandler):
6    def on_modified(self, event):
7        if not event.is_directory:
8            print(f"Modified: {event.src_path}")
9
10    def on_created(self, event):
11        if not event.is_directory:
12            print(f"Created: {event.src_path}")
13
14    def on_deleted(self, event):
15        if not event.is_directory:
16            print(f"Deleted: {event.src_path}")
17
18if __name__ == "__main__":
19    path = "."  # directory to watch
20    observer = Observer()
21    observer.schedule(ChangeHandler(), path, recursive=True)
22    observer.start()
23    print(f"Watching {path} for changes. Press Ctrl+C to stop.")
24    try:
25        while True:
26            time.sleep(1)
27    except KeyboardInterrupt:
28        observer.stop()
29    observer.join()

The Observer runs in a background thread, so your main thread remains free for other work. You can register multiple handlers for different directories on the same observer.

Node.js -- chokidar

Node.js ships a built-in fs.watch function, but its behavior varies across platforms and it does not handle edge cases like atomic saves or symlinks well. The chokidar library solves these problems and has become the standard choice. Install it with npm install chokidar.

javascript
1const chokidar = require('chokidar');
2
3const watcher = chokidar.watch('.', {
4  ignored: /(^|[\/\\])\../,   // ignore dotfiles
5  persistent: true,
6  ignoreInitial: true,
7});
8
9watcher
10  .on('add',    path => console.log(`Created: ${path}`))
11  .on('change', path => console.log(`Modified: ${path}`))
12  .on('unlink', path => console.log(`Deleted: ${path}`));
13
14console.log('Watching current directory for changes...');

Chokidar normalizes events across Linux, macOS, and Windows and adds features such as glob-based ignoring, initial scan suppression, and debouncing of rapid consecutive changes.

Polling as a Fallback

When native APIs are unavailable, for example on network-mounted filesystems (NFS, SMB) or inside certain containers, you can fall back to polling. Both watchdog and chokidar support a polling mode:

  • watchdog: replace Observer with PollingObserver from watchdog.observers.polling.
  • chokidar: pass { usePolling: true, interval: 1000 } in the options.

Polling checks file metadata (modification time, size) at a fixed interval. It consumes more CPU than event-based watching and introduces a detection delay equal to the poll interval.

Common Pitfalls

  • Hitting the watch limit on Linux. The default max_user_watches (often 8192) is too low for large projects. Increase it with echo 65536 | sudo tee /proc/sys/fs/inotify/max_user_watches.
  • Ignoring editor temp files. Many editors write to a temporary file and rename it over the original (atomic save). This produces a delete + create pair instead of a modify event, which can confuse naive handlers.
  • Watching network filesystems with event APIs. inotify and FSEvents do not work on NFS or SMB mounts. You must use polling mode on these filesystems.
  • Forgetting to debounce rapid changes. A single file save can trigger multiple events within milliseconds. Without debouncing, your handler may run expensive operations redundantly.
  • Leaving zombie watchers. Failing to call observer.stop() (Python) or watcher.close() (Node.js) before exit leaks file descriptors and can exhaust OS resources over time.

Summary

  • Every major OS provides a kernel-level file notification API: inotify on Linux, FSEvents on macOS, and ReadDirectoryChangesW on Windows.
  • Python's watchdog and Node.js's chokidar wrap these APIs behind a consistent cross-platform interface.
  • Use polling mode as a fallback when native events are not available, such as on network-mounted filesystems.
  • Always debounce rapid events, handle atomic-save patterns, and clean up watchers on exit.
  • Increase the OS watch limit before monitoring large directory trees on Linux.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.