Stopping the 'Clean Build' Cycle: Mastering the Gradle Build Cache
Stop wasting time on 'clean builds.' Learn how to use the Gradle Build Cache to reuse task outputs locally and across CI, and how to avoid common pitfalls like absolute path leaks.
20 Sept 2025, 08:41 UTC

The Cost of the Clean Build
Many development teams rely on ./gradlew clean build as a safety blanket. When a build behaves unexpectedly or a dependency change doesn't seem to propagate, the instinct is to wipe the build/ directory and start over. While this ensures correctness, it destroys the local state, forcing Gradle to re-execute every single task—compilation, test execution, and resource processing—which can turn a 30-second change into a 10-minute wait.
The Gradle Build Cache solves this by decoupling the output of a task from the current workspace. Unlike standard incremental builds, which only work if the build/ folder is intact, the build cache stores task outputs in a separate directory (or a remote server). If the inputs to a task haven't changed, Gradle simply pulls the result from the cache, even after a clean command or on a fresh CI agent.
Local vs. Remote Caching
Gradle implements caching at two levels. The Local Cache resides on your machine (typically in ~/.gradle/caches/build-cache-1). It allows you to switch git branches and return to a previous state without recompiling everything.
The Remote Cache is a shared backend (via HTTP or a dedicated Gradle Enterprise node). This is where the real engineering win happens: when a CI server builds a feature branch, it pushes the outputs to the remote cache. When a developer pulls that branch, their local machine downloads the pre-compiled binaries instead of building them locally.
Putting it into Practice
To enable the build cache locally, add the following to your gradle.properties file:
org.gradle.caching=true
Alternatively, you can trigger it for a single run using the --build-cache flag.
Worked Example: Verifying Cache Hits
To see the cache in action, run a task that typically takes significant time, such as tests. Run these commands from your project root with standard user permissions:
- First Run:
./gradlew test --build-cache(Tasks will execute normally). - The Wipe:
./gradlew clean(This deletes the localbuild/folder). - Second Run:
./gradlew test --build-cache.
In the second run, you should see FROM-CACHE next to the task names in the console output. This indicates that Gradle bypassed execution entirely and restored the test results from the cache directory.
The "Cache Poisoning" Trap
Caching only works if a task is deterministic. If a task produces different output for the same input, it can "poison" the cache with incorrect results. Consider this custom task that writes the current timestamp into a version file:
tasks.register('generateVersion') {
doLast {
file('version.txt').text = "Build date: ${new Date()}"
}
}
Because new Date() changes every millisecond, the output is never the same. If this task is marked as cacheable, it will either never hit the cache or, worse, provide a stale timestamp to downstream tasks. To avoid this, ensure task inputs are explicitly declared and avoid using non-deterministic data (like timestamps or random IDs) inside the task execution logic.
The Relocatability Hurdle
A common reason for cache misses in shared environments is absolute paths. If a task records the full path to a source file (e.g., /Users/dev-a/project/src/Main.java) into a generated manifest, that output is only valid for dev-a. When dev-b pulls that output from the remote cache, Gradle detects the path mismatch and triggers a cache miss.
To fix this, use @PathSensitive(PathSensitivity.RELATIVE)` on task inputs. This tells Gradle to calculate the cache key based on the relative path from the project root, making the output relocatable across different machines and directories.
Limitations and Verification
The build cache is not a magic bullet. It increases disk usage in the .gradle folder and, in the case of remote caches, adds network overhead. If the time to download a large artifact exceeds the time to compile it locally, the cache becomes a bottleneck.
How to verify your cache health:
- Run your build with
--infoto see exactly why a task was not taken from the cache. - Use a Gradle Build Scan to visualize cache hits and misses across your entire pipeline.
- Check the local cache directory to ensure entries are being created after a successful build.
Rollback
If you encounter corrupted build outputs, you can clear the local cache by deleting the ~/.gradle/caches/build-cache-1 directory. This forces Gradle to re-calculate all outputs on the next run.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.