skip to content

What ingredients make up a cacheable task's build cache key, and how can two builds that you think are identical end up with different keys?

level: seniorimportance: should knowfreq 44%

answer

  1. impl hash + inputs + file fingerprints
  2. plugin/buildSrc bump invalidates
  3. annotation processor via classpath
  4. absolute path / timestamp leakage
  5. --scan / --info to diagnose

basics

~20 s

The key hashes the task's implementation (type + classpath), each @Input value, and the fingerprint of all file inputs. Any difference — a changed annotation processor, a different input file content/path, or a plugin upgrade — changes the key.

solid answer

~40 s

A cache key is the combined hash of: (1) the **task implementation** — the task class plus the classpath of the plugin/buildSrc that defines it and any nested action classes; (2) every declared **input property** (`@Input`, `@Nested`) value; and (3) the **fingerprint of file inputs** (`@InputFiles`/`@InputFile`/`@InputDirectory`/`@Classpath`), which mixes content hashes with normalized path info per the chosen `@PathSensitive`/`@Classpath` rules. Surprises come from inputs you didn't realize are inputs: bumping a plugin or `buildSrc` changes the implementation hash for *every* task it defines; an annotation processor or compiler version flows in via the classpath; an undeclared input (env var, absolute path baked into a property) either silently breaks correctness or, once declared, legitimately changes the key. Use `--build-cache --info` or a build scan to see why keys differ.

code

bash · 5 lines
bash
# Inspect why cache keys diverge between two runs
./gradlew :app:compileJava --build-cache --info | grep -i 'cache key'

# Richer per-task cache-key breakdown
./gradlew build --build-cache --scan

go deeper

for a junior

Recall that the key depends on inputs and that changing an input changes the key.

for a middle

Name the three components and that file fingerprints mix content + normalized path.

for a senior

Explain implementation-hash churn from plugin/buildSrc bumps and diagnose divergence with scans/--info.

for a principal

Govern cache-key stability across many plugins, control buildSrc churn, and set CI hit-rate targets and reproducibility standards.

## The three pillars of a cache key Gradle computes a cacheable task's key from three hashed components: 1. **Task implementation hash.** The task *class* and the *classloader/classpath* that loaded it (the plugin jar, `buildSrc`, or applied script). If you upgrade the plugin that defines the task, or change `buildSrc` code, the implementation hash changes and *all* of that task's cache entries are invalidated — even if your source didn't change. Nested action/`@Nested` types and `doFirst`/`doLast` closures' implementation also count. 2. **Input property values.** Each `@Input` (and the recursively-walked `@Nested` object graph) contributes its value's hash. Strings, numbers, enums, `Provider` values resolved at execution, etc. 3. **File input fingerprints.** For each file input property, a fingerprint combining per-file **content hashes** with **path info** normalized by `@PathSensitive` / `@Classpath` / `@CompileClasspath`. Order can matter (`@Classpath`) or not, and irrelevant entries can be filtered. ## 'Identical' builds with different keys — common causes - **Plugin / buildSrc upgrade.** New plugin version ⇒ new implementation hash ⇒ misses. - **Toolchain / processor change.** A different JDK, annotation processor, or compiler arg enters via a classpath or `@Input` and shifts the key. - **Absolute path leakage.** Default `ABSOLUTE` sensitivity (or an absolute path stored in an `@Input` string) makes the key machine-specific. - **Non-reproducible inputs.** A timestamp, build number, or `System.currentTimeMillis()` captured into an `@Input` changes every run. - **Volatile environment.** A value read from the environment and declared as input legitimately differs per machine. ## Diagnosing ```bash # show task input hashes / why something wasn't cached ./gradlew build --build-cache --info # or generate a build scan for a per-task cache-key breakdown ./gradlew build --build-cache --scan ``` A build scan's timeline lists each task's *build cache key* and which inputs contributed, making it straightforward to compare two builds and find the diverging input. ## Why it matters Understanding key composition is the difference between a cache that hits 90%+ on CI and one that quietly never hits. Most 'the cache doesn't work' incidents trace to one of the divergence causes above — usually an undeclared or non-reproducible input, or an implementation-hash churn from frequently-changing `buildSrc`.

  • Why does bumping a plugin version cause a wave of cache misses even without source changes?
    The plugin jar is part of the task implementation classpath, so its hash feeds the key. A new version changes the implementation hash for every task that type defines, invalidating their cache entries.
  • How would you stop a build timestamp from busting the cache every run?
    Don't capture it as an @Input. Move volatile metadata out of cache-keying inputs, or make outputs reproducible (e.g. set a fixed timestamp in archives) so identical content yields identical keys.
  • What's the fastest way to see which input changed between two builds?
    A build scan: compare the two scans' task cache keys and input fingerprints, or run with --info to print input-hash details.

saying these in an interview costs you the question

  • Listing only file inputs and forgetting the task implementation/classpath component.
  • Claiming source-only changes are the only thing that invalidates a key.
  • Storing non-reproducible values (timestamps, random IDs) as @Input and being surprised by misses.

context