You enabled file system watching but warm builds aren't faster and you see 'retained 0 files' — how do you systematically troubleshoot it?
answer
- verbose retained vs invalidated
- ./gradlew --status for daemon PIDs
- stable org.gradle.jvmargs
- inotify max_user_watches
- NFS/container mounts unsupported
- check for --no-watch-fs override
basics
~20 sConfirm 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 sTreat 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./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 invalidatedgo deeper
Know to enable verbose logging and check that watch is on.
Check daemon reuse, filesystem type, and the inotify quota.
Run a structured three-class diagnosis with --status and verbose output, and know CI nuance.
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.