A task you didn't write (e.g. a third-party plugin's task or `test`) keeps missing the remote cache despite no real change. How do you diagnose and fix it via normalization/path sensitivity from the consumer side?
answer
- -Dorg.gradle.caching.debug=true / build scan inputs diff
- classify: absolute path / volatile jar content / empty dirs
- global normalization.runtimeClasspath fixes consumers you don't own
- validate from a 2nd checkout path
- convention plugin + hit-rate CI gate
basics
~20 sUse build-cache debug logging or the build scan to compare input fingerprints between two runs, find which input changed (often an absolute path or a build-stamped file in a jar), then stabilize it with runtime classpath normalization or by adjusting the input's path sensitivity.
solid answer
~50 sStart by confirming it's a fingerprint difference, not a non-cacheable task: enable `-Dorg.gradle.caching.debug=true` (or read the build scan's task inputs comparison) to see, per input, which fingerprint changed between a cache-populating run and a missing run. Common culprits: (1) an **absolute path** entering the key because an input uses ABSOLUTE sensitivity or an absolute path is passed as a plain input — fix by making it RELATIVE/relative-to-root or wrapping it so the path is normalized; (2) a **volatile file inside a jar** (build-info/git.properties, manifest Built-By) — fix with `normalization.runtimeClasspath { ignore(...) / properties{ ignoreProperty } / metaInf{ ignoreAttribute } }`; (3) **empty directories** differing between CI and local — fix by ensuring inputs use `@IgnoreEmptyDirectories`. Since you don't own the task, you apply project-level normalization (global) and configure the plugin's inputs via its DSL where exposed. Validate by re-running on a clean checkout at a different path and confirming a hit.
code
bash · 4 lines# Populate cache, then prove a hit from a different checkout path
./gradlew test --build-cache -Dorg.gradle.caching.debug=true
# move/clone repo to a different absolute path, then:
./gradlew test --build-cache # expect: Task :test FROM-CACHEgo deeper
Know that a build scan / caching debug log shows which input changed and that some misses come from timestamps inside jars.
Diagnose via caching.debug, identify the changed input, and apply the matching fix (normalization or path sensitivity).
Systematically classify the three causes, apply project-level normalization for tasks you don't own, and validate from a second checkout.
Drive an org-level remediation: a convention plugin applying standard normalization + pre-annotated task types, plus a CI hit-rate gate and governance against unsafe relaxations.
## Step 1 — confirm the failure mode A cache miss has two broad causes: the task is **not cacheable** (no `@CacheableTask`/no declared outputs) or it **is cacheable but its input fingerprint differs**. Path-sensitivity/normalization only helps the second. Check the build scan's *Build cache* and *Task inputs* sections, or run with `-Dorg.gradle.caching.debug=true`, which logs each input's individual hash. Re-run twice (ideally on two checkout paths/machines) and diff the per-input hashes to find the offending input. ## Step 2 — classify the offending input Three recurring causes, all fixable from the consumer side: ### a) Absolute path leaks into the key Symptom: the same content yields different keys on CI vs. local, or after moving the checkout. The input is fingerprinted with ABSOLUTE sensitivity (or an absolute path string was passed as a plain `@Input`). For an input you configure, switch to a relative declaration; for plugin tasks, prefer passing values via `layout`/`Provider` so paths are project-relative, and avoid feeding absolute `File` paths as scalar inputs. ### b) Volatile content inside an artifact Symptom: a runtime-classpath consumer (e.g. `test`) misses whenever an upstream jar is rebuilt, because the jar embeds a timestamp/commit. Fix globally: ```kotlin normalization { runtimeClasspath { properties("META-INF/build-info.properties") { ignoreProperty("build.time") } metaInf { ignoreAttribute("Built-By"); ignoreAttribute("Build-Jdk") } } } ``` This applies to **every** runtime classpath in the project, so you don't need to own the consuming task. ### c) Empty-directory noise Symptom: CI (clean Git checkout, no empty dirs) misses what local populated, or vice versa. If you control the input wiring, add `@IgnoreEmptyDirectories`; if it's a plugin's input, see whether the plugin exposes the input as a file collection you can reconfigure, or stabilize the working tree. ## Step 3 — validate the fix Reproduce the original miss deterministically: populate the cache from one checkout path, then build from a **second checkout at a different absolute path** (and ideally a different machine/container) with `--build-cache`. A `FROM-CACHE` outcome confirms the fingerprint is now stable. Add an assertion or a periodic cache-hit-rate report so regressions surface. ## Governance angle At org scale, low hit rates usually trace to a handful of recurring anti-patterns: stamped build metadata without normalization, absolute paths in inputs, and environment-specific empty dirs. The durable fix is a **shared convention plugin** that applies the standard `normalization` block and provides task types pre-annotated with RELATIVE + `@IgnoreEmptyDirectories`, plus a CI gate that reports hit rate so regressions are caught early. ## Distinguishing the tools - `@PathSensitive` → path portion of arbitrary file inputs. - `normalization.runtimeClasspath` → entry/content portion of classpath inputs. - `@IgnoreEmptyDirectories` → empty-dir participation. Diagnosis tells you which knob applies; never relax a knob past what's correct for the task just to force a hit.
- The task isn't cacheable at all — does normalization help?No. Normalization and path sensitivity only stabilize input fingerprints for already-cacheable tasks. If the task lacks @CacheableTask or declared outputs, you must make it cacheable first (or it's intentionally non-cacheable).
- How do you fix this for a plugin task whose inputs you can't annotate?Use project-level normalization.runtimeClasspath (it applies to all runtime classpaths regardless of who declared them), reconfigure inputs through the plugin's DSL where exposed, and avoid feeding absolute paths into its scalar inputs.
- How do you prove the fix worked?Populate the cache from one checkout, then build --build-cache from a second checkout at a different absolute path/machine and confirm the task reports FROM-CACHE; track hit rate over time to catch regressions.
saying these in an interview costs you the question
- Disabling the cache or marking inputs @Internal to 'fix' misses — that hides real changes and is a correctness bug.
- Assuming all misses are fingerprint issues without checking the task is even cacheable.
- Relaxing path sensitivity past what's correct just to force a hit.