A custom cacheable task gets cache hits locally but never on CI. As a senior engineer, how do you reason about and fix the cause?
answer
- local hit + CI miss = non-relocatable
- absolute path -> RELATIVE
- timestamp/SHA file -> normalization ignore
- classpath -> @Classpath
- caching.debug diff per-input hashes
basics
~20 sLocal hits but cross-machine misses almost always mean the cache key isn't relocatable: absolute paths, machine-specific input content, or timestamps leak in. Fix with relative path sensitivity and input normalization so keys match across machines.
solid answer
~50 sHits locally (same checkout) but misses on CI is the classic **non-relocatability** signature: something machine-specific is in the cache key, so the same logical inputs hash differently on two machines. I'd reason through the candidate leaks: (1) **absolute paths** — file inputs at `@PathSensitive(ABSOLUTE)` (or a default that keeps absolute paths) embed the checkout directory; switch to `RELATIVE`. (2) **volatile file content** — generated resources with timestamps/build numbers/commit hashes; strip them with `normalization { runtimeClasspath { ignore(...) } }` or by not feeding them as inputs. (3) **classpath order/timestamps** — use `@Classpath`/`@CompileClasspath` instead of plain `@InputFiles`. (4) **environment-dependent inputs** — JDK path, OS, env vars captured as `@Input`. I'd confirm by comparing the two keys: a build scan or `-Dorg.gradle.caching.debug=true` prints each input's hash, so I diff local vs CI to see which input differs, then normalize exactly that one. The principle: the key must encode semantics, not the machine.
code
bash · 4 lines# Capture per-input hashes on each machine, then diff to find the leak
./gradlew :app:myCacheableTask \
-Dorg.gradle.caching.debug=true --info 2>&1 | tee local-keys.txt
# (run same commit on CI, save ci-keys.txt, diff the two)go deeper
Recognize the symptom (works locally, not on CI) points to paths in the key, but may not lead the full diagnosis.
List the common leaks (absolute paths, timestamps, classpath order) and apply RELATIVE + normalization fixes.
Systematically diff per-input hashes via caching debug/build scan, identify the exact leak, and fix without introducing false hits.
Drive normalization standards and CI seeding strategy so cross-machine relocatability holds across the whole org, and review for correctness regressions.
## The signature **Local cache hits, zero CI hits** means each machine computes a *different* cache key for inputs that are logically identical. The task is cacheable (it stores entries) but the entries aren't **relocatable**. The whole value of a shared/remote cache is cross-machine reuse, so this is the bug that makes caching useless in practice. ## Reasoning checklist (most common first) 1. **Absolute paths in file inputs.** If a file input keeps absolute path information, `/home/ci/agent/work/...` differs from `/Users/dev/proj/...`. Fix: annotate file inputs `@PathSensitive(PathSensitivity.RELATIVE)` (or `NONE`/`NAME_ONLY` where appropriate). 2. **Volatile content inside inputs.** A `build-info.properties`, manifest with `Build-Date`, or a generated file embedding the git SHA changes every build, so the input hash changes every time. Fix: `normalization { runtimeClasspath { ignore("...") ; metaInf { ignoreAttribute("...") } } }`, or don't declare the volatile file as an input. 3. **Classpath noise.** Jar entry order and intra-jar timestamps differ across machines. Fix: declare classpaths with `@Classpath`/`@CompileClasspath` (which normalize), not plain `@InputFiles`. 4. **Environment captured as input.** A `@Input` holding `System.getProperty("java.home")`, an absolute tool path, or `System.getenv()` leaks the machine. Fix: capture only the semantic value (e.g. JDK *version*, not its path) or mark genuinely irrelevant ones `@Internal`. 5. **Output declarations including absolute-path side data** that round-trips into the next task's input. ## How to confirm — diff the keys The decisive move is to make the two keys observable and diff them: ```bash ./gradlew myTask -Dorg.gradle.caching.debug=true --info ``` With caching debug on, Gradle logs, per task, each input property and its individual hash. Capture this on a local run and a CI run for the same commit, then diff. Exactly the input whose hash differs is your leak — you don't have to guess. Build scans present the same per-input hashes in a nicer UI and can compare two builds. ## The mental model A cache key should encode the **semantics** of the inputs (what content/values actually affect the output), never the **environment** that produced them (paths, timestamps, machine identity). Every fix above is an instance of removing environment from the key while preserving semantics. Over-correcting — normalizing away something that *does* affect the output — swings you into **false hits**, so each normalization must be justified against "does this change the result?" ## After the fix Once keys match, CI runs that push to the shared cache seed entries that developers (and other agents) restore. Validate by running the task on a clean checkout on a second machine and confirming `FROM-CACHE`.
- How would you actually pinpoint which input differs between local and CI rather than guessing?Enable -Dorg.gradle.caching.debug=true (or use a build scan) to log each input property's individual hash, run the same commit on both machines, and diff the per-input hashes. The property whose hash differs is the non-relocatable leak.
- After fixing path sensitivity you now get a CI hit that produces wrong output. What likely happened?You over-normalized — e.g. set NONE/NAME_ONLY or ignored a resource that genuinely affects the result — so two different real inputs collide on one key (a false hit). Tighten normalization back so the meaningful input is in the key.
Like hashing a function with the machine's hostname accidentally mixed in — same arguments, different hash on every host, so no two machines ever share a memo.
saying these in an interview costs you the question
- Blaming the remote cache server instead of the task's key composition.
- Fixing by disabling caching rather than making the key relocatable.
- Over-normalizing to force hits and introducing false hits / wrong outputs.