Skip to content

Research notes / File change detection

What inotify misses: mapped writes and collapsed change events

Published / Measured API behavior

A quiet notification queue does not establish that file contents are unchanged. In our Linux probe, a shared mapped write changed 32 bytes without an IN_MODIFY event, while 32 ordinary writes could collapse into one event. Closing an unchanged writable file still produced IN_CLOSE_WRITE.

On this page6 sections

The same data change can produce different event counts

We ran eight cases five times on each of two mounts, ext4 and tmpfs, on Linux 6.8.0-146-generic: 80 case executions on one host. Every repetition agreed with the table below. These are checks of controlled behavior, not a sample from which to estimate a backup failure rate.

Both mount types produced these counts. Each fresh file contained 4,096 bytes. The watch requested only IN_MODIFY and IN_CLOSE_WRITE.
CaseChanged bytesMODIFY before closeCLOSE_WRITE after close
One pwrite111
32 pwrites; drain at end3211
32 pwrites; drain each time32321
Shared mmap + flush3201
Private mmap + flush001
Open writable, no modification001
Watch grandparent directory100
Watch immediate directory111

The two 32-write cases performed the same one-byte overwrites at offsets 0 through 31. Only the queue-reading schedule changed. Reading after each completed call yielded 32 modification events; leaving the queue unread until the end yielded one. Event count therefore cannot serve as a write-operation counter.

The mapped cases stored the same 32 bytes and called Python's synchronous mapping flush. With a shared mapping, the complete file comparison found the edits. With a private mapping, the edits remained private and the file was unchanged. Neither generated a modification event in the observed pre-close phase. The mapping contract explains why the two modes have different effects.

A close event is a useful hint, not a content assertion

In every file-watch case, the original writable descriptor remained open while we checked the bytes and read the queue. We then closed it and read the queue again. A close-write event appeared after shared modification, after private modification and after doing nothing at all. No close-write events appeared in the earlier phase.

The upstream Linux v6.8 fsnotify_close() hook selects its close event using FMODE_WRITE. It does not first compare old and new contents. [2] That explains why the no-write control matters: the event's name is not a promise that bytes changed. Conversely, a long-lived mapped writer need not close promptly, so waiting for closure cannot provide a bounded change-detection delay.

The directory controls used root/child/payload, fully created before installing the watch. Watching root yielded neither selected event for a write to payload; watching child yielded both. This isolates watch coverage without relying on a race while creating directories.

Linux documents unread-event coalescing, nonrecursive directory watches and the omission of mapped modifications. It also documents queue overflow, identified by IN_Q_OVERFLOW, as a reason an application's cached view may need rebuilding. [1] We did not induce overflow: the probe rejects an overflow record rather than counting a partial stream as complete.

The indexing problem predates modern cloud backup

Buettcher and Clarke's 2006 Wumpus report describes how per-directory registration can race with newly created subtrees. It also asks for changed-byte ranges instead of a coarse file-change signal. Its experimental fschange interface was a research patch, not a current inotify feature. The report is valuable design history, not a reason to install that old patch. [3, pp. 31-33]

ViewBox, published at FAST 2014, addresses a different layer: detecting a changed file does not establish that its bytes are valid or that several files form a consistent recovery state. Its historical tests inject corruption and crash states; its design combines a checksumming ext4 variant with consistent views for synchronization. [4, sections 2-4]

Our probe does not rerun ViewBox or assess any current cloud client. The useful connection is architectural: discovery, integrity and consistent capture are separate obligations. Improving the notification consumer alone does not satisfy the other two.

Use events to schedule work; retain a way to check state

For a backup or indexer, our engineering recommendation is to treat notifications as candidates for reconciliation, not as a complete transaction history. The results support three concrete checks:

  1. Test the writers you actually run. Include shared mappings and long-lived descriptors, not only small files written and immediately closed.
  2. Separate activity from coverage. Record the watched scope, gaps and successful reconciliation separately from event totals. The grandparent control demonstrates how an active watch can miss the relevant file.
  3. Define the capture contract. A rescan can discover changed state, but reading a live tree is not automatically an application-consistent snapshot. Use a suitable snapshot or application protocol when recovery requires one.

Repeated hints can be combined into a pending-file work item to avoid redundant rereads. Any such optimization must still handle a new change arriving during the read; clearing a pending flag after processing without checking for newer work can discard it. This is a design recommendation, not a benchmark or an implementation tested here.

For the next stages, see snapshot and recovery scope and backup verification and restoration. The corpus entry for inotify provides the compact definition and source links.

Run the probe on your own mount

Download the Python probe and complete JSON observations. Requirements are Linux, Python 3 and findmnt; no Python packages or privileges are needed. Choose writable test directories:

python3 inotify-observation.py --directory . --directory /dev/shm --repetitions 5 --output results.json

Each case uses a private temporary directory and a fresh file, installs its watch after setup, verifies the whole payload, and records raw events plus SHA-256 hashes. The final queue read waits up to 50 ms if nothing is ready and then drains to EAGAIN. No concurrent writer is involved. Mount types, kernel, Python, libc, page size and the probe's hash accompany the results.

No device cache eviction, power interruption, rename race, network filesystem, overflow stress or end-to-end backup was tested. A matching cached read is evidence of visible contents, not proof of survival after power loss. The two mount types are on the same host and are not independent hardware populations.

Primary sources

  1. Linux man-pages 6.19: inotify(7), Notes and Limitations. Accessed 2026-10-19.
  2. Linux v6.8: fsnotify_close selects close events by file mode. Accessed 2026-10-19.
  3. Buettcher and Clarke: Adding Full-Text Filesystem Search to Linux, ;login: 31(3), 2006, pp. 28-33. Accessed 2026-10-19.
  4. Zhang, Dragga, Arpaci-Dusseau and Arpaci-Dusseau: ViewBox, FAST 2014, pp. 119-132. Accessed 2026-10-19.

Published by Hesela team · Published · Updated

Report a correction · Editorial standards · Reuse guidance

Change history

: first published.

: latest recorded update. A detailed change note is not available for this revision.

New-publication RSS · Correction RSS

Cite this page
Hesela team. What inotify misses: mapped writes and collapsed change events. Hesela. 2026-10-19. https://hesela.com/analyses/inotify-change-detection/