skip to content

What are overlapping outputs, and why can they silently disable caching for a task even when its key is computed correctly?

level: seniorimportance: nice to knowfreq 25%

answer

  1. two tasks -> same output dir
  2. Gradle can't attribute files
  3. caching disabled for correctness
  4. key still computed fine
  5. fix: dedicated output dirs; see --info

basics

~20 s

Overlapping outputs are when two tasks write into the same directory. Gradle can't tell which files belong to which task, so it disables caching for the affected task to avoid packing the wrong files — even though its key is fine.

solid answer

~50 s

Gradle packs a cacheable task's outputs by capturing exactly the files in its declared `@OutputDirectory`/`@OutputFile`. If **two tasks write into the same output location** (overlapping outputs), Gradle cannot safely determine which files are 'this task's' versus the other's, so it **refuses to cache** the affected task — it would otherwise pack a neighbor's files or fail to restore cleanly. The key may compute perfectly; caching is still skipped for correctness. You'll see this as a task that's cacheable, has stable inputs, yet never shows FROM-CACHE. `--info` reveals a message like 'Caching disabled ... because Gradle does not know how file ... was created (output property ... )' or that the task has overlapping outputs. The fix is to give each task its own dedicated output directory (e.g. `layout.buildDirectory.dir("generated/<task>")`) so outputs are disjoint. This commonly bites custom tasks and some legacy plugins that dump into a shared `build/` subfolder.

code

bash · 4 lines
bash
./gradlew :app:generateX --build-cache --info \
  | grep -i "Caching disabled"
# > Caching disabled for task ':app:generateX' because:
# >   Gradle does not know how file '.../build/shared/x' was created

go deeper

for a junior

Know that two tasks writing to the same folder can break caching.

for a middle

Explain that Gradle disables caching to preserve output correctness and how to give each task its own output dir.

for a senior

Add this to the diagnosis ladder after key inspection and read --info/Build Scan non-cacheable reasons.

for a principal

Mandate disjoint output directories as a build-hygiene standard and audit plugins that violate it.

## Why output ownership matters for caching To store a cache entry, Gradle must **pack** precisely the files a task produced. It knows the task's declared output locations, and after execution it snapshots those locations. The packed archive is later **unpacked** to restore outputs. This only works if Gradle can attribute each file in an output location to exactly one task. ## Overlapping outputs defined **Overlapping outputs** occur when more than one task declares (or writes into) the *same* directory or file. Example: `taskA` and `taskB` both have `@OutputDirectory` = `build/shared`. Now if `taskA` runs, then `taskB`, the `build/shared` directory contains a mix. Gradle cannot tell which files belong to `taskA` when packing its cache entry — and on restore it might clobber `taskB`'s files. To avoid silently corrupting outputs, Gradle **disables caching** (and sometimes up-to-date checking) for the overlapping tasks. Crucially, the **cache key is still computed correctly** — the inputs are fine. The disqualification happens at the *output ownership* stage, which is why the symptom is confusing: stable inputs, cacheable annotation present, yet always executed. ## Detecting it Run with `--info` and look for messages such as: ``` Caching disabled for task ':generateX' because: Gradle does not know how file 'build/shared/...' was created (output property '...'). Task output caching requires exclusive access to output paths to guarantee correctness. ``` or an explicit 'overlapping outputs' note. A Build Scan also flags non-cacheable reasons per task. ## The fix Give every task a **disjoint, dedicated** output location: ```kotlin tasks.register<GenerateTask>("generateA") { outDir.set(layout.buildDirectory.dir("generated/a")) } tasks.register<GenerateTask>("generateB") { outDir.set(layout.buildDirectory.dir("generated/b")) } ``` If a third-party plugin causes the overlap, you may need to reconfigure its output dir or file an upstream issue. Avoiding shared output folders is also a general hygiene rule: it makes incremental build, parallelism, and `clean` semantics correct, not just caching. ## Relationship to key diagnosis When diagnosing 'never FROM-CACHE' tasks, after confirming inputs are stable via `-Dorg.gradle.caching.debug=true`, the *next* suspect is output ownership: check `--info` for caching-disabled reasons before assuming the key is the problem.

  • If the cache key is correct, why does the task still never hit?
    Key correctness is separate from output ownership. Overlapping outputs make Gradle unable to safely pack/restore, so it disqualifies the task from caching regardless of a valid key.
  • How do you confirm overlapping outputs are the cause?
    Run with --info and look for a 'Caching disabled ... overlapping outputs / does not know how file was created' message, or read the non-cacheable reason in a Build Scan.

saying these in an interview costs you the question

  • Assuming a stable key guarantees a cache hit
  • Pointing multiple tasks at one shared build/ subfolder
  • Ignoring --info caching-disabled messages while chasing the key

context