The complete guide

Gradle remote build cache: setup, CI strategy, and managed options

A Gradle remote build cache lets CI and developers reuse task outputs produced on another machine. This guide covers HttpBuildCache configuration, the recommended trusted-CI push model, how to prove a real FROM-CACHE hit, and how to choose between a managed cache and operating your own backend.

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: 5071 for HTTP build cache traffic, 6011 for 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 HttpBuildCache is 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 --info

The 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 only

Put 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 = false on the remote block (or guard it on an environment variable) and keep local on. 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 call outputs.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 buildCache block, or a plugin that finalises the configuration itself in addition to your settings.gradle.kts.
  • The same configuration applied from two places - a buildCache {} block in settings.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=true or pass --build-cache.
  • The task is not cacheable. Lifecycle and some copy-like tasks may be intentionally uncacheable. Inspect--info output 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_TOKEN is present in the Gradle process environment.
  • Reads work but writes do not. isPush may be false, or the token may correctly be read-only.

Related guides

Try a managed Gradle remote build cache
Free for developers. Use Gradle's native HttpBuildCache and keep your existing build.
Start freeSee pricing
FAQ

Gradle remote build cache: frequently asked questions

What is a Gradle remote build cache?
A Gradle remote build cache stores cacheable task outputs on a shared server. When another developer or CI runner has the same task inputs, Gradle downloads those outputs and reports FROM-CACHE instead of executing the task again.
How do I enable the Gradle build cache?
Set org.gradle.caching=true in gradle.properties or pass --build-cache. Then configure remote<HttpBuildCache> in settings.gradle.kts, or remote(HttpBuildCache) in settings.gradle, with the cache URL and credentials.
Should developer builds push to the remote Gradle cache?
Usually no. Gradle recommends a CI-populated pattern: trusted CI builds push entries while developer and pull-request builds read them. This limits conflicting writes and reduces cache-poisoning risk.
How do I verify a Gradle remote cache hit?
Run a cacheable task once to populate the cache, clean the local outputs and local cache, then run it again with --info. A successful remote hit is shown as FROM-CACHE without the task action running again.
Can Cachely host a Gradle remote build cache?
Yes. Cachely implements the Gradle HTTP Build Cache protocol. Point HttpBuildCache at https://remote.cachely.dev and use a workspace token as the HTTP Basic password. No Gradle plugin or custom task runner is required.
Why is my Gradle task not loaded FROM-CACHE?
The task may not be cacheable, the build cache may be disabled, inputs may differ across machines, outputs may not be reproducible, or authentication may be failing. Use --info to see whether Gradle skipped lookup, missed, or rejected the remote cache.
How do I disable the Gradle build cache?
Pass --no-build-cache to turn it off for a single run, which overrides org.gradle.caching for that invocation. To disable it persistently, set org.gradle.caching=false in gradle.properties. To keep the local cache but drop the remote one, set isEnabled = false on the remote block in settings.gradle.kts. To stop caching one specific task, annotate it with @DisableCachingByDefault or call outputs.doNotCacheIf(...) - correct for tasks whose outputs are not a deterministic function of their declared inputs.
How do I clear the local Gradle build cache?
Delete ~/.gradle/caches/build-cache-1. ./gradlew clean does not clear it - clean removes build outputs, which is exactly what makes the next run a cache hit rather than a fresh compile. You can also let Gradle expire entries itself with local { removeUnusedEntriesAfterDays = 7 } in the buildCache block. Clearing the local directory has no effect on the remote cache, so a "cleared" cache can still produce FROM-CACHE.
What is the Gradle build cache node, and is it still supported?
The Develocity Build Cache Node (the gradle/build-cache-node Docker image, also shipped as an executable JAR) is a self-hosted Gradle and Maven cache server that listens on port 5071 for HTTP build cache traffic and 6011 for Bazel gRPC. It is deprecated: the Develocity 2025.2 release notes announce discontinuation at the end of 2026, its manual states it will not be distributed, supported, or available after 31 December 2026, and Develocity 2027.1 drops support. Gradle directs users to Develocity Edge. Confirm current dates with Gradle before planning a migration. Checked July 2026.
Do I need a dedicated Gradle build cache server?
No. Gradle talks to any backend that implements its HTTP build cache protocol, so a dedicated server is one option rather than a requirement. Self-hosting means you own authentication, TLS, eviction, disk headroom, and upgrades, including replacing a deprecated component before its end-of-support date. A managed HttpBuildCache endpoint removes that operational surface, and Cachely provides one without a Gradle plugin.
What is the difference between a Gradle remote build cache and distributed builds?
A remote build cache skips work by downloading task outputs that were already produced for identical inputs. Distributed builds and test distribution spread work that genuinely has to run across several machines. Caching addresses repeated work such as incremental CI and rebuilding unchanged modules; distribution addresses large suites on a cold cache. Enable caching first, because a cache hit avoids the compute entirely, then distribute what remains.
What is org.gradle.caching?
org.gradle.caching is the Gradle property that turns the build cache on. It is off by default; set org.gradle.caching=true in the project's checked-in gradle.properties so laptops and CI agree, or pass --build-cache to force it on for one run and --no-build-cache to force it off. It controls the build cache only and is unrelated to org.gradle.configuration-cache, which caches the configuration phase.
What does "Build cache controller already set" mean in Gradle?
It is an internal IllegalStateException thrown when Gradle's build cache configuration is finalized more than once for the same build. Common causes are a plugin or init script that reconfigures the build cache after settings are evaluated, the same buildCache block applied from two places (for example settings.gradle.kts plus an init script), or embedded/TestKit sessions that reuse build state. Fix it by configuring the build cache in exactly one place and removing any script or plugin that re-applies it; read the stack trace to see which one does the second configuration.