Backend Development 10 min read

File Change Monitoring in Java: WatchService Pitfalls, JDK Bug, and Inotify Alternatives

This article examines a real‑world file‑watching failure caused by a JDK bug, explains how Java's WatchService works (including its polling fallback), compares it with Linux's inotify mechanism, and presents practical work‑arounds for reliable configuration reloads in backend services.

IT Services Circle
IT Services Circle
IT Services Circle
File Change Monitoring in Java: WatchService Pitfalls, JDK Bug, and Inotify Alternatives

Hello everyone, I'm XiaoLou. I recently investigated a file‑change‑watching issue in a configuration‑distribution service and documented the findings.

From a fault to the background: A service distributes configuration files to clients via an agent; the client watches the local file for changes to reload configuration. The initial implementation used a dedicated thread that periodically polls the file's last-modified timestamp (millisecond precision) to detect changes.

Drawbacks of this simple approach:

Change detection is not real‑time; it depends on the polling interval.

Millisecond precision can miss rapid successive changes within the same millisecond.

Although these issues rarely caused problems, a serious production incident occurred due to a JDK bug that caused File.lastModified() to lose millisecond precision and return only whole‑second timestamps. The bug is documented at JDK‑8177809 .

To illustrate the bug, I ran a demo on JDK 1.8.0_261 and JDK 11.0.6 (both on macOS). The results showed that the timestamp granularity differed between the two versions, confirming the bug.

WatchService – the built‑in Java file‑change monitor

After learning about the bug, I explored Java's WatchService . A quick demo shows how to register a directory and handle CREATE, MODIFY, DELETE, and OVERFLOW events:

public static void watchDir(String dir) {
    Path path = Paths.get(dir);
    try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
        path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
                StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE,
                StandardWatchEventKinds.OVERFLOW);
        while (true) {
            WatchKey key = watchService.take();
            for (WatchEvent
watchEvent : key.pollEvents()) {
                if (watchEvent.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
                    System.out.println("create..." + System.currentTimeMillis());
                } else if (watchEvent.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
                    System.out.println("modify..." + System.currentTimeMillis());
                } else if (watchEvent.kind() == StandardWatchEventKinds.ENTRY_DELETE) {
                    System.out.println("delete..." + System.currentTimeMillis());
                } else if (watchEvent.kind() == StandardWatchEventKinds.OVERFLOW) {
                    System.out.println("overflow..." + System.currentTimeMillis());
                }
            }
            if (!key.reset()) {
                System.out.println("reset false");
                return;
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Running this demo on macOS revealed a strange delay: after the first file modification, the MODIFY event was reported only after about 9.5 seconds. This hinted that the underlying implementation was not truly event‑driven.

WatchService internals

Debugging showed that FileSystems.getDefault().newWatchService() returned an instance of sun.nio.fs.PollingWatchService on macOS, which periodically polls the file timestamps (default 10 seconds). The source code confirms that it uses the same timestamp‑polling logic as our original implementation, meaning it inherits the same bug on platforms that lack native support.

On Linux, the implementation switches to sun.nio.fs.LinuxWatchService , which leverages the kernel's inotify system call for true event‑driven monitoring.

inotify – Linux kernel file‑change mechanism

Linux's inotify provides efficient, real‑time notifications. A Java program can use it indirectly via the native LinuxWatchService . However, on macOS the native implementation is unavailable, so Java falls back to the polling strategy.

To verify the use of inotify , I traced the Java process with strace -f -o s.txt java FileTime , which showed inotify system calls on Linux.

How the production issue was fixed

Because both the configuration distributor and the consumer were under our control, we bypassed the buggy timestamp check by adding a separate version file (e.g., an MD5 hash of the configuration). The consumer now watches the version file; when its content changes, the actual configuration file is reloaded.

We considered using WatchService directly, but learned that in Docker environments the inotify implementation can lose events, a problem not specific to Java.

Conclusion

Even well‑tested APIs can harbor subtle bugs that surface under specific conditions. Understanding the underlying mechanisms (polling vs. native event‑driven) is essential for building reliable backend services.

BackendJavaLinuxinotifyfile monitoringJDK BugWatchService
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.