skip to content

You enabled file system watching but warm builds aren't faster and you see 'retained 0 files' — how do you systematically troubleshoot it?

level: seniorimportance: should knowfreq 22%

answer

  1. verbose retained vs invalidated
  2. ./gradlew --status for daemon PIDs
  3. stable org.gradle.jvmargs
  4. inotify max_user_watches
  5. NFS/container mounts unsupported
  6. check for --no-watch-fs override

basics

~20 s

Confirm the daemon is reused (stable JVM args, not --no-daemon), the workspace is on a local supported filesystem, and on Linux the inotify quota is high enough. Use org.gradle.vfs.verbose to see retained vs. invalidated counts.

solid answer

~40 s

Treat it as three failure classes. **(1) No live daemon to retain the VFS:** check you're not using `--no-daemon`, that `org.gradle.jvmargs` is stable (varying args spawn a fresh daemon), and that the daemon isn't expiring or being killed between builds — run `./gradlew --status`. **(2) Unsupported filesystem:** NFS/SMB, some Docker bind/overlay mounts, and VM-shared folders don't deliver native events; move the project to a local disk. **(3) Watch-backend limits:** on Linux the inotify quota (`fs.inotify.max_user_watches`) may be exceeded for a large repo, disabling watching with a warning — raise the sysctl. Drive the diagnosis with **`org.gradle.vfs.verbose=true`**: a healthy warm build shows high retained / low invalidated; persistent zero-retained points at one of the three causes. Also confirm `org.gradle.vfs.watch` isn't overridden to false in a `gradle.properties` or by `--no-watch-fs` in CI scripts.

code

bash · 3 lines
bash
./gradlew --status                 # is one daemon reused across builds?
cat /proc/sys/fs/inotify/max_user_watches   # Linux quota check
./gradlew build -Dorg.gradle.vfs.verbose=true   # retained vs invalidated

go deeper

for a junior

Know to enable verbose logging and check that watch is on.

for a middle

Check daemon reuse, filesystem type, and the inotify quota.

for a senior

Run a structured three-class diagnosis with --status and verbose output, and know CI nuance.

for a principal

Codify environment standards (local-disk workspaces, sysctl limits, stable JVM args) and decide where VFS effort is worthwhile vs. investing in remote build cache.

## Step 0: confirm it's actually enabled Check for an override: a project or `~/.gradle/gradle.properties` setting `org.gradle.vfs.watch=false`, or a `--no-watch-fs` flag in a wrapper/CI script. Then turn on diagnostics: ```properties org.gradle.vfs.verbose=true ``` Run the build twice and read the second run's 'retained information about X files' line. High retained = working; ~0 retained = a problem in one of the classes below. ## Class 1 — the daemon isn't being reused The VFS lives in daemon memory, so retention needs the **same daemon** across builds. - Are you running `--no-daemon`? Then there is never a retained VFS. - Is `org.gradle.jvmargs` (or any daemon-affecting flag) **changing between invocations**? Each distinct set spawns a new daemon, each starting cold. Standardize it. - Is the daemon **expiring** (idle timeout, low-memory expiry) or being **killed** by an IDE/script? Inspect with: ```bash ./gradlew --status # lists live daemons, their PIDs and state ``` If you see a new PID each build, retention can't help. ## Class 2 — unsupported / remote filesystem Native events aren't delivered on many non-local filesystems: NFS, SMB/CIFS, certain **Docker bind/overlay mounts**, and VM-shared folders (older Vagrant/VirtualBox shares). Gradle either won't watch them or sees no events, so everything is re-snapshotted. The remedy is to host the workspace on a **local, supported filesystem**. ## Class 3 — backend resource limits (mostly Linux inotify) inotify watches per directory and is capped by `fs.inotify.max_user_watches`. A large monorepo can blow past the default, and registration fails with a warning; watching is then disabled for that build. ```bash cat /proc/sys/fs/inotify/max_user_watches sudo sysctl fs.inotify.max_user_watches=524288 # persist in sysctl.conf ``` ## Putting it together | Symptom | Likely cause | Fix | |---|---|---| | New daemon PID each build | unstable JVM args / --no-daemon / expiry | stabilize args, keep daemon, raise idle timeout | | Warning about watch service on Linux | inotify quota exceeded | raise max_user_watches | | Zero retained on a mounted volume | NFS/SMB/container/VM mount | move workspace to local disk | | watch=false in logs | property/flag override | remove the override | ## CI nuance On CI, zero retention is often **expected and acceptable**: fresh agents and one-build-per-container mean no daemon reuse. Don't chase retention there; the build cache (local+remote) is the relevant cross-machine optimization instead.

  • Each build shows a different daemon PID in --status. Why does that defeat watching?
    A new daemon starts with an empty in-memory VFS, so there's nothing retained from the prior build. Usually it's caused by changing org.gradle.jvmargs or other daemon-affecting flags, or daemon expiry/kills; stabilize the args and keep the daemon alive.
  • On CI you see retained 0 every time — is that a bug to fix?
    Usually not. CI agents rarely reuse a daemon between jobs, so cross-build retention can't apply. Rely on the build cache for cross-run speed-ups there instead of chasing VFS retention.

saying these in an interview costs you the question

  • Jumping straight to raising inotify limits without confirming the daemon is even reused.
  • Treating CI zero-retention as a defect.
  • Forgetting to check for a vfs.watch=false / --no-watch-fs override.

context