Article View

Scroll down to read the full article.

The Silent Stalker: Node.js `fs.watch` Failing on Kubernetes NFS/EFS with Bottlerocket

calendar_month August 07, 2026 |
Quick Summary: Troubleshoot Node.js `fs.watch` and `chokidar` silent failures on EKS Bottlerocket nodes using NFS/EFS. Uncover `inotify` limits and fix silent co...

Alright, listen up. You've got a Node.js application running happily on your local machine, picking up config changes, hot-reloading like a champ. Then you deploy it to Kubernetes on EKS, using Bottlerocket OS, mounting your shared config or content via NFS (EFS, whatever). Suddenly, your application is blind. Config changes? Ignored. Hot-reloads? Non-existent. No errors in the logs, just… nothing. Welcome to the infuriating world of silently failing file watchers.

This isn't some obscure Node.js bug, not directly. It’s a gnarly interaction between specific kernel versions, Node.js's underlying libuv, and the default limits imposed by Linux's inotify subsystem. When it happens, your app just stops seeing filesystem events. No crash, no loud error. Just silence.

A dark
Visual representation

The Symptom: Your App Is Deaf to Change

The core symptom is simple: anything relying on file system watching (like fs.watch, chokidar, webpack's watch mode, or dynamic config loaders) just… stops working after a while, or never starts working reliably under load. You change a file, and your application never reacts. It's stable, not crashing, but functionally crippled.

You'll pull your hair out checking network mounts, permissions, even Node.js versions. You'll blame EFS latency. You'll wonder if it's the same EIO issue we debugged with fs.readdir on NFSv4, but you won't see an EIO. This is worse: it's a ghost in the machine.

The Environments Where This Fester

This problem is particular. It typically surfaces in environments with high file watch activity and specific kernel configurations, especially within container orchestration platforms.

Component Affected Versions/Configurations Notes
Host OS Kernel Linux Kernel < 5.15 (especially older Bottlerocket OS versions like 1.7.x - 1.9.x) Newer kernels (5.15+) often have higher defaults or better handling.
Node.js Runtime Node.js 16.x, 18.x, 20.x The specific Node.js version is less critical than libuv's interaction with the kernel.
Filesystem NFSv4/EFS mounts, other network filesystems Aggravated by these, but can occur on local filesystems with enough watches.
Container Orchestration Kubernetes (EKS specifically on Bottlerocket) Defaults on managed node groups are often conservative.

You’re essentially hitting the wall of kernel resource limits without explicit error reporting to the application layer. Not great.

The Root Cause

The problem is the Linux kernel's inotify watch limits. Specifically, fs.inotify.max_user_watches and fs.inotify.max_user_instances. Each process (or rather, each user, but within a container, it's typically one process/user) can only register a finite number of directory or file watches. Every time Node.js's fs.watch or a library like chokidar sets up a watch, it consumes one of these resources.

When the application attempts to add a new watch and the limit is already exhausted, the underlying inotify_add_watch syscall fails with ENOSPC. Here's the kicker: libuv (what Node.js uses) and consequently Node.js itself, often don't translate this specific failure into a JavaScript error that gets thrown or emitted. It simply fails to add the watch. Your application thinks it has a watcher in place, but the kernel silently refused it. It's a fundamental architectural oversight in how these resource exhaustion scenarios are communicated across layers.

Network filesystems like NFS or EFS can sometimes exacerbate this because watching deep directory trees, especially large ones, can quickly consume thousands of watch descriptors. If your application or a dependency tries to watch `node_modules` on an EFS volume, you're doomed.

A cluttered
Visual representation

The Fix: Increase inotify Limits

Since the problem is a kernel limit, the solution is to increase those limits on the host operating system. This is not something you can generally do from inside an unprivileged container. You need to modify the sysctl parameters on the underlying Kubernetes worker nodes.

For Bottlerocket OS on EKS, this means you'll need to use a DaemonSet with privileged permissions or apply an appropriate Bottlerocket HostContainer. The goal is to set fs.inotify.max_user_watches and fs.inotify.max_user_instances to sufficiently high values.

A good starting point for a busy Node.js app, especially one using chokidar on a large shared filesystem, would be 262144 for watches and 1024 for instances. These numbers might seem high, but for applications that aggressively watch file trees, they are necessary. We've seen setups require even higher for large enterprise applications. Remember, a single user can have multiple watch instances.

If you're dealing with hundreds of microservices or large monolithic apps, this is where engineering humility comes into play when scaling distributed systems. Don't assume defaults will hold. Know your kernel.

Here’s how you'd typically apply this via a privileged Kubernetes DaemonSet:


apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: inotify-limiter
  namespace: kube-system
spec:
  selector:
    matchLabels:
      name: inotify-limiter
  template:
    metadata:
      labels:
        name: inotify-limiter
    spec:
      hostNetwork: true # Not strictly required, but often used for DaemonSets that touch host settings
      containers:
      - name: inotify-limiter-container
        image: ubuntu:latest # Or any lightweight image with sysctl
        command: ["/bin/sh", "-c"]
        args:
          - |
            echo "Increasing inotify limits..."
            sysctl -w fs.inotify.max_user_watches=524288
            sysctl -w fs.inotify.max_user_instances=2048
            # Apply changes permanently for future reboots if not using a host container config for Bottlerocket
            echo "fs.inotify.max_user_watches=524288" >> /etc/sysctl.conf
            echo "fs.inotify.max_user_instances=2048" >> /etc/sysctl.conf
            sysctl -p
            echo "Inotify limits set. Sleeping indefinitely."
            sleep infinity
        securityContext:
          privileged: true # CRITICAL: This DaemonSet needs privileged access to modify host sysctl
        volumeMounts:
          - name: sysctl-conf
            mountPath: /etc/sysctl.conf
      volumes:
        - name: sysctl-conf
          hostPath:
            path: /etc/sysctl.conf
            type: FileOrCreate # Ensure the file exists if it doesn't
      nodeSelector:
        kubernetes.io/os: linux # Target Linux nodes

Warning: Running privileged DaemonSets requires extreme caution. Ensure you understand the security implications. Alternatively, for Bottlerocket, consider using a HostContainer with appropriate configurations, which is the recommended Bottlerocket way to manage host settings.

Verification

After applying the fix, ensure your applications are restarted on the affected nodes. Then, verify the limits from within a pod on the node:


kubectl exec -it <your-pod-name> -- cat /proc/sys/fs/inotify/max_user_watches
kubectl exec -it <your-pod-name> -- cat /proc/sys/fs/inotify/max_user_instances

These commands should now reflect the higher values. Your application should start detecting file changes again without complaint. If you're still seeing issues, check dmesg on the host itself for any other inotify related errors or other resource exhaustion messages. This particular bug taught us that silent failures are the worst kind. Good luck out there, you'll need it.

Discussion

Comments

Read Next