What is a Gradle remote build cache?
Gradle's build cache reuses the outputs of cacheable tasks when their inputs are unchanged. The local cache helps repeated builds on one machine. A remote build cache adds a shared HTTP backend, so an output created by CI can be downloaded by a developer, and an ephemeral runner can reuse work from an earlier pipeline.
This is task output caching, not dependency caching. It can skip compilation, tests, code generation, and other cacheable work instead of merely restoring downloaded dependencies. On a hit, Gradle restores the declared outputs and reports FROM-CACHE. On a miss, it runs the task normally and may upload the result. For the tool-neutral model, see the complete guide to remote build caching.
One switch turns the whole thing on. The Gradle build cache is off by default; org.gradle.caching=true enables the local cache, and a remote backend layered on top makes those outputs shareable. Everything below builds on that single property, covered in full under enabling, disabling, and clearing the cache.
Gradle HttpBuildCache setup
Enable task output caching in gradle.properties, then configure Gradle's built-in HttpBuildCache in the settings file. No plugin is required:
# gradle.properties
org.gradle.caching=true// settings.gradle.kts
buildCache {
remote<HttpBuildCache> {
url = uri("https://remote.cachely.dev")
isPush = System.getenv("CI") != null
credentials {
username = ""
password = System.getenv("GRADLE_CACHE_TOKEN")
}
}
}The Groovy DSL uses remote(HttpBuildCache), push, and the same credentials block. With Cachely, the token is the HTTP Basic password and the username is ignored. Export GRADLE_CACHE_TOKEN locally and inject it from your CI secret store. The same token can serve Nx, Lerna through Nx, Turborepo, Gradle, and Bazel in one workspace.
CI pushes; developers and pull requests read
Gradle's recommended operating model is deliberately asymmetric: trusted CI builds populate the remote cache, while developer machines consume entries without pushing. This creates a stable stream of outputs from a controlled environment and keeps untrusted builds from writing artifacts that protected builds may later reuse.
- Protected branch CI: use a read-write token and set
isPush = true. - Developer laptops: leave
isPush = false; they still download remote hits. - Pull requests and forks: use a server-enforced read-only token even if a configuration mistake enables push.
That last control matters because a build cache is part of the software supply chain. Read-only credentials for untrusted builds prevent cache poisoning at the server boundary. Cachely also isolates workspaces and stores managed artifacts under content-derived keys.
Gradle build cache server options
"Gradle build cache server" covers three genuinely different things, and the first one is on a deadline. Any Gradle cache backend that speaks HttpBuildCache works, so the choice is operational rather than technical.
Develocity Build Cache Node (deprecated)
The component many teams still mean by "Gradle cache server" is the Build Cache Node, distributed as the gradle/build-cache-node Docker image and as an executable JAR. Gradle has deprecated it: per the Develocity 2025.2 release notes it is discontinued at the end of 2026, the documentation states it will not be distributed, supported, or available after 31 December 2026, and Develocity 2027.1 drops support entirely. The documented migration path is Develocity Edge.
If you are standing up a cache server today, this matters more than any performance comparison: plan for the successor rather than the deprecated node. Verify the current dates yourself in the Build Cache Node manual and the Develocity 2025.2 release notes before committing, since vendor timelines move. Checked July 2026.
- Runtime: Java 21 for node 19.0 and later; roughly 1.5 GiB of memory by default.
- Ports:
5071for HTTP build cache traffic,6011for Bazel remote caching over gRPC. - Storage: one data directory per node, 10 GiB default cache size, and a data directory cannot be shared between nodes - which is what makes horizontal scaling an operational project rather than a flag.
Your own HTTP server
Because HttpBuildCache is an ordinary HTTP protocol - GET to read an entry, PUT to store one, both keyed by Gradle's cache key - you can put any compliant server behind it, including a reverse proxy in front of object storage. The protocol is the easy part. The work is authentication, TLS, read-only credentials for untrusted builds, eviction, disk headroom, and being paged when the cache is the reason CI is red.
A managed endpoint
A managed HttpBuildCache URL removes the server from your estate entirely: no node to upgrade before a deprecation date, no storage to size. Cachely implements the native protocol, so the switch is a URL and a token rather than a plugin or a build rewrite - and the same workspace also serves Nx, Turborepo, and Bazel.
Managed vs self-hosted Gradle build cache
Self-hosted HTTP cache
Gradle's protocol is straightforward enough to run yourself. You control region, retention, and storage, but you also own authentication, TLS, availability, cleanup, monitoring, and safe read/write credential separation. This can be the right answer for on-premises requirements or teams that already operate build infrastructure.
Develocity
Develocity is Gradle's full developer productivity platform, including build scans, performance analytics, test distribution, and build cache capabilities. Choose it when you want the wider platform rather than only a remote cache.
Managed standalone cache
A managed HttpBuildCache endpoint is the focused option when task output caching is the requirement and operating another stateful service is not.Cachely implements the native protocol, requires no Gradle plugin, offers a free developer tier, and reports cache activity alongside Nx, Turborepo, and Bazel.
Remote build cache vs distributed builds
These get conflated, and picking the wrong one wastes a quarter. A remote build cache skips work: if a task's inputs were built before, anywhere, its outputs are downloaded instead of recomputed. Distributed execution spreads work: it farms tasks or tests out across several machines so the wall-clock time of work that genuinely has to run drops.
- Cache wins when work repeats. Incremental CI on a shared branch, developers pulling main, and rebuilding unchanged modules are all repeated work. This is most teams' largest source of waste, it is the cheapest to fix, and
HttpBuildCacheis built into Gradle. - Distribution wins when work is genuinely new and parallel. A large test suite on a cold cache cannot be skipped, so spreading it across agents is the only lever. Develocity Test Distribution is the mature option here, and it is the better choice for that problem - Cachely does not distribute execution.
They compose, and the order matters: cache first, because a hit costs a download and distribution still costs the compute. Measure how much of your build is repeated work before paying to parallelise it.
Verify a real FROM-CACHE hit
Use a test that eliminates the local cache as an explanation:
# First run: execute and upload a cacheable task
./gradlew clean compileJava --build-cache --info
# Clear local state, then run the same inputs again
rm -rf ~/.gradle/caches/build-cache-*
./gradlew clean compileJava --build-cache --infoThe second run should show compileJava FROM-CACHE. If you are validating across CI and a laptop, keep source, Gradle version, JDK, operating assumptions, and relevant environment inputs aligned. A different input creates a different key and correctly misses.
Enabling, disabling, and clearing the Gradle build cache
org.gradle.caching and the command-line override
The build cache is off by default. org.gradle.caching=true in gradle.properties turns it on for every invocation, and the CLI flags override that property per run - so you never have to edit a file to change your mind:
# gradle.properties - the persistent setting
org.gradle.caching=true
./gradlew build --build-cache # force on, whatever the property says
./gradlew build --no-build-cache # force off for this run onlyPut org.gradle.caching in the project's checked-in gradle.properties so laptops and CI agree; a value that only exists in ~/.gradle/gradle.properties on one machine is a classic reason two developers get different results from the same commit. Note that this property controls the build cache only - it is unrelated to org.gradle.configuration-cache, which caches the configuration phase and is a separate feature.
Disabling the cache without disabling caching
"Disable the Gradle build cache" usually means one of three narrower things, and picking the narrow one keeps the rest of your build fast:
- Disable it for one run -
--no-build-cache. The right move when you are bisecting a suspected bad cache entry. - Disable only the remote cache - set
isEnabled = falseon theremoteblock (or guard it on an environment variable) and keeplocalon. Useful for offline work or when the cache server is down, though a reachable cache should already degrade to a normal build rather than fail one. - Disable caching for one task - annotate a custom task with
@DisableCachingByDefault(because = "..."), or calloutputs.doNotCacheIf(...)for a conditional case. This is correct for tasks whose outputs are not a deterministic function of their declared inputs; caching those produces wrong results, not just misses.
Clearing local cache state
The local build cache is a directory of content-addressed entries, not something ./gradlew clean touches - clean deletes build outputs, which is exactly what makes the next run a cache hit rather than a fresh compile:
rm -rf ~/.gradle/caches/build-cache-1 # empty the local build cache
// settings.gradle.kts - let Gradle expire entries itself
buildCache {
local { removeUnusedEntriesAfterDays = 7 }
}Deleting the local directory has no effect on the remote cache, so the next build can still restore the same outputs over the network - which is the behaviour you want, and the reason a "cleared" cache can still produce FROM-CACHE. To prove a task really executes, combine --no-build-cache with clean.
The "Build cache controller already set" error
Build cache controller already set is not a caching-quality problem, it is a configuration-lifecycle one, and it stops the build rather than just missing the cache. Gradle finalises the build cache configuration once per build and builds an internal controller from it; the IllegalStateException is thrown when something tries to finalise that configuration a second time for the same build.
The usual triggers are all forms of "the build cache is being configured more than once":
- A plugin or init script that reconfigures the build cache after settings have already been evaluated - for example programmatically re-applying a
buildCacheblock, or a plugin that finalises the configuration itself in addition to yoursettings.gradle.kts. - The same configuration applied from two places - a
buildCache {}block insettings.gradle(.kts)and another in an init script or a convention plugin, so the tree tries to build the controller twice. - Embedded or reused Gradle sessions - TestKit runs and other embedded/nested invocations that reuse build state across runs can drive the configuration through its lifecycle more than once.
The fix is to configure the build cache in exactly one place. Keep a single buildCache {} block in settings.gradle.kts (or a single init script - not both), and remove any plugin or script that re-applies or finalises the build cache configuration after that. Because this is an internal Gradle exception rather than a documented user-facing setting, confirm the exact cause for your version by reading the stack trace - it points at the plugin or script doing the second configuration - and search the Gradle issue tracker if a specific plugin is implicated.
Troubleshooting Gradle remote cache misses
- Caching is disabled. Set
org.gradle.caching=trueor pass--build-cache. - The task is not cacheable. Lifecycle and some copy-like tasks may be intentionally uncacheable. Inspect
--infooutput before blaming the backend. - Inputs differ. JDKs, absolute paths, environment variables, generated files, or non-reproducible outputs can split keys across machines.
- The URL or token is wrong. Use the bare Cachely host and verify
GRADLE_CACHE_TOKENis present in the Gradle process environment. - Reads work but writes do not.
isPushmay be false, or the token may correctly be read-only.
Related guides
- What is a remote cache?The pillar guide to content-hashed keys, local vs remote caching, and safety.
- Develocity alternativeWhen a focused managed Gradle cache beats a broad productivity platform.
- Bazel remote cacheHTTP AC/CAS setup, read-only PR builds, and cache-miss troubleshooting.
- Managed remote build cacheHow to evaluate a managed cache on protocol support, security, and cost.