Skip to content
Latchkey
Published June 2026 by Kaveh Alemi

The State of Gradle Builds 2026

Gradle has the most sophisticated incrementality system in any build tool, and ephemeral CI throws nearly all of it away on every job.

71%
reduction in median build time at a high remote-cache hit rate
Latchkey analysis (modeled)
2.9x
configuration time saved by the configuration cache on warm runs
Latchkey analysis (modeled)
0%
daemon reuse on a default ephemeral CI runner, every job starts cold
Latchkey analysis (modeled)

Executive summary

Gradle has spent more than a decade building one of the most sophisticated incrementality systems in any build tool. A local and remote build cache, a long-lived daemon that keeps the JVM hot and the build model loaded, a configuration cache that skips the configuration phase entirely, and fine-grained incremental compilation that recompiles only what changed. On a developer laptop these features compound into near-instant rebuilds, and that experience is what JVM developers think of when they think about Gradle performance.

In CI, most teams throw nearly all of it away, and they do so structurally rather than by choice. The reason is the shape of a default CI runner: it is ephemeral, it boots fresh for every job, and it is discarded when the job ends. A daemon that is designed to live across builds cannot survive a runner that does not. A local cache that accelerates the second build is empty on a runner that only ever does one. A configuration cache that pays off on the warm run is cold on the only run an ephemeral runner ever performs.

The result is that CI pays the cold-everything tax on every single job while the laptop pays it once. This report quantifies what each Gradle acceleration feature actually returns when it works, why a high remote build-cache hit rate is the single biggest lever a CI pipeline has, and how the cost curve bends when a runner can persist a warm environment across jobs. The throughline is that Gradle's incrementality is not broken in CI; it is simply starved of the warm state it was designed to exploit.

The remote build cache is the one feature that escapes the ephemeral-runner problem on its own, because it is shared infrastructure rather than runner-local state. A runner with an empty local cache and a cold daemon can still skip work that any other runner has already done, provided the remote cache hit rate is high. That is why hit rate, not daemon tuning or JVM flags, is the number that decides Gradle CI performance, and why the gap between a 40 percent and a 95 percent hit rate is the difference between a slow pipeline and a fast one.

The practical conclusion is that the highest-leverage Gradle CI investment is the one that turns the ephemeral-runner case back into the laptop case: a warm shared cache that persists across jobs, a high remote build-cache hit rate, and automatic recovery for the transient failures that otherwise force a full re-run. Together they recover daemon-class, configuration-cache-class, and incremental-compilation-class savings that an ephemeral runner cannot deliver on its own, without hand-rolling and babysitting self-hosted infrastructure.

Median build time vs remote cache hit rate
0% hit (cold)410 s40% hit286 s75% hit168 s95% hit119 s

Wall-clock for a multi-module JVM build as the remote build-cache hit rate rises. · Source: Latchkey analysis (modeled)

Time saved per Gradle acceleration feature
Daemon reuse12%Configuration cache19%Incremental compile24%Remote build cache58%

Modeled wall-clock saved on a warm run versus a fully cold baseline build. · Source: Latchkey analysis (modeled)

Email me the report

The full report is right here on this page, free. Want the link in your inbox to read later or share, plus new Latchkey reports as they drop? Drop your email and we will send it over.

Sent! Check your inbox for the report link.

No spam. Unsubscribe anytime.

The remote build cache is the whole game

Of every Gradle acceleration feature, the remote build cache returns the most CI time by a wide margin, and the reason is architectural rather than incidental. Every other feature, the daemon, the configuration cache, incremental compilation, depends on runner-local state that an ephemeral CI runner does not keep. The remote build cache is the only one that lives outside the runner, which means a fresh runner with a cold daemon and an empty local cache can still skip work that any other runner, anywhere, has already done.

The hit-rate curve is steep and it is the most important chart in this report. A cold build with a 0 percent hit rate sits near 410 seconds for the modeled multi-module project. At a 40 percent hit rate it drops to roughly 286 seconds, at 75 percent to about 168, and at 95 percent to around 119 seconds, a 71 percent reduction versus cold. The relationship is not linear; the high end of the curve is where the savings concentrate, which is why pushing hit rate from good to excellent is worth real effort.

Every other Gradle feature is a refinement layered on top of this one lever. Daemon reuse, configuration caching, and incremental compilation each shave a meaningful slice off a warm run, but none of them approaches the build cache's contribution, and none of them helps at all on the cold ephemeral runner where they have no warm state to exploit. The operational takeaway is blunt: watch the remote build-cache hit rate the way you watch test coverage, because it is the number that decides Gradle CI performance.

  • The remote build cache is shared infrastructure, so it works even on a cold, fresh runner with no local state.
  • Hit-rate gains are non-linear; the move from 75% to 95% is where a large share of the savings lives.
  • A 95% hit rate cuts modeled build wall-clock by roughly 71% versus a fully cold build.

The daemon dies on every ephemeral runner

Gradle's daemon is engineered to stay alive across builds. It keeps the JVM warmed up so the JIT has already optimized the hot paths, it holds the configured build model in memory so Gradle does not have to reconstruct it, and it carries file-system watch state that makes incremental work cheap. On a developer laptop the daemon is reused dozens of times a day, and that reuse is a large part of why the second and subsequent builds feel instant.

On a default ephemeral CI runner that boots fresh for every job and is discarded when the job ends, daemon reuse is effectively zero. Every CI build pays the full cold-JVM tax: class loading, JIT warmup from scratch, and a fresh build-model construction, none of which the daemon was supposed to make you pay twice. The feature exists, it is enabled, and it returns nothing, because the runner architecture denies it the one thing it needs, which is a process that outlives a single build.

Recovering daemon-class savings in CI requires a runner that persists a warm environment across jobs, and the realistic options are either hand-rolled self-hosted infrastructure or a managed runner that keeps a warm pool. Self-hosting gets you the warm daemon at the cost of owning the patching, scaling, and cleanup that a runner fleet demands continuously. A managed warm pool delivers the same warm-environment benefit without the operational burden, which is why daemon reuse in CI is really an infrastructure decision rather than a Gradle configuration one.

The configuration cache is underused in CI

The configuration cache skips Gradle's configuration phase entirely on a warm run by serializing the result of configuring the build and replaying it. On a large multi-module project the configuration phase is a meaningful slice of total time, roughly 18 percent of a cold build in the modeled split, and on builds with many modules and heavy plugin configuration it can be considerably more. Skipping it on every run after the first is a real and repeatable saving.

Teams reliably enable the configuration cache locally, where it pays off across the many builds a single developer runs, and then leave it cold in CI, where it pays off only if the cache is shared across runners. Because the default ephemeral runner does not share state, the configuration cache in CI is cold on the only run each runner performs, and the feature that triples configuration-phase throughput on a warm run returns nothing on a cold one.

Persisting the configuration cache alongside the build cache on a runner that keeps warm state turns this around. On warm runs the configuration phase is replayed rather than recomputed, which is the source of the roughly 2.9x configuration-time saving in the key stats. The configuration cache does not need anything from the build that the build cache does not also need; both want the same thing, which is a runner whose state survives the end of a job.

Where a cold Gradle build spends time
Compilation 44%
Configuration phase 18%
Test execution 26%
Dependency resolution 12%

Estimated split of a cold multi-module build with no cache reuse. · Source: Latchkey analysis (modeled)

Incremental compilation needs a warm working tree

Incremental compilation is the feature that makes Gradle feel fast on a laptop. When the previous compiled outputs are present, Gradle recompiles only the sources that changed and the sources that depend on them, rather than the whole module. Compilation is the single largest phase of a cold build, roughly 44 percent in the modeled split, so the difference between recompiling everything and recompiling a handful of changed files is enormous.

The catch is that incremental compilation has nothing to be incremental against on a clean checkout. An ephemeral runner clones the repository fresh, with no previous compiled outputs and no Gradle user home carried over, so the very first compile is also the only compile, and it is a full one. The feature that saves the most time on a warm working tree is precisely the feature that an ephemeral runner cannot use, because it deletes the working tree between jobs.

Keeping the compiled outputs and the Gradle user home on a warm shared volume that persists across runs is what turns incremental compilation from a laptop-only feature into a CI feature. Combined with a high remote build-cache hit rate, the effect compounds: the build cache supplies outputs for modules that any runner has already built, and incremental compilation handles the modules that genuinely changed, so the runner does the minimum work the diff actually requires rather than rebuilding the world on every job.

Compilation and tests dominate the cold build, and both are cacheable

The cold-build split is a useful map of where the time goes when none of Gradle's incrementality is working. Compilation is the largest phase at roughly 44 percent, test execution is next at about 26 percent, the configuration phase is around 18 percent, and dependency resolution is the remaining 12 percent or so. The striking thing about this breakdown is that every one of these phases is addressable by caching or incrementality that an ephemeral runner happens to defeat.

Compilation is handled by the build cache and incremental compilation; test execution is handled by build-cache reuse of unchanged test outputs and by test-result caching; the configuration phase is handled by the configuration cache; and dependency resolution is handled by a persistent Gradle user home. The cold build is expensive not because the work is fundamentally slow but because the runner forces every phase to start from nothing.

This is why the warm and cold numbers diverge so sharply, and why the fix is infrastructural rather than a matter of tuning any single phase. A team that obsesses over compiler flags or test parallelism on a cold ephemeral runner is optimizing the wrong layer; the larger win is making the runner stop discarding the state that every one of these phases was designed to reuse.

  • Compilation is the largest cold-build phase, and it is exactly what the build cache and incremental compile accelerate.
  • Test execution and dependency resolution are both cacheable across runs given a persistent Gradle user home.
  • The cold build is expensive because the runner discards reusable state, not because the work is inherently slow.

A high hit rate plus a managed runner cuts the bill

Stacking a high remote-cache hit rate onto a managed runner is where the Gradle cost curve actually bends, and the per-build cost chart shows the four positions. A hosted runner on a cold cache is the worst case at roughly forty-one cents per build; a hosted runner on a warm cache improves to around eighteen cents; a self-hosted warm runner lands near fourteen; and a managed runner with a warm shared cache comes in around six cents, the lowest by a wide margin.

Two effects compound to produce that number. The compute itself is roughly 70% cheaper than hosted rates, so every minute costs less. And a high cache hit rate means most modules never compile at all, so there are far fewer minutes to pay for in the first place. A cheaper minute multiplied by far fewer minutes is how a forty-one-cent cold build becomes a six-cent warm one.

The last piece is keeping the saved time from leaking back out through transient failures. Gradle CI is prone to flaky integration tests and dependency-resolution timeouts that force a full, expensive re-run of the whole build. Automatic retry of these transient failures on a fresh environment absorbs them before they trigger a re-run, so the time the warm cache saved is not handed straight back to a registry blip or a Testcontainers startup race, and change-failure rate stays inside the elite 0-15% band.

Cost per Gradle build by runner path
Hosted, cold cache$0.41Hosted, warm cache$0.18Self-hosted, warm$0.14Managed (Latchkey)$0.06

Modeled cost of one CI build across runner choices, cold vs warm cache. · Source: GitHub Actions pricing + Latchkey rates

Cache correctness is a precondition, not an afterthought

A build cache only helps if teams trust it, and trust is earned through correctness. Gradle's cache keys are derived from task inputs, so a task that fails to declare an input correctly can serve a stale cached output and produce a build that is fast and wrong. The most damaging cache problem is not a low hit rate, it is a high hit rate on a cache that occasionally returns the wrong answer, because that erodes confidence and pushes teams to disable caching entirely.

The discipline that prevents this is treating task input and output declarations as a first-class part of the build rather than an afterthought. Tasks that read configuration, environment, or files they do not declare as inputs are the usual source of incorrect cache hits, and Gradle's own cacheability checks and build scans surface these. The teams that run a high hit rate safely are the teams that audit cacheability rather than assuming it.

Correctness also intersects with the warm-runner story. A clean, well-isolated environment per job removes a class of false cache behavior caused by leftover state bleeding between builds, so the same managed runner that delivers warm caches also makes those caches more trustworthy. The fast path and the correct path point at the same architecture, which is part of why the warm-shared-cache model is durable rather than a shortcut.

What the fastest Gradle teams do differently

The JVM teams with the fastest Gradle CI are not running exotic configurations. They have accepted that the ephemeral-runner default defeats Gradle's incrementality, and they have built their pipeline around restoring the warm state those features need. The habits are unglamorous and they compound, and none of them require rewriting the build logic.

They also treat the remote build-cache hit rate as the headline metric of their build infrastructure. They watch it over time, they investigate when it drops, and they audit cacheability so the hit rate they have is a correct one. Because they can see the number, they catch a misdeclared task input or a cache regression before it slows every build on the team, rather than discovering it months later as a vague sense that CI got slower.

  • Run a remote build cache and push the hit rate toward 95%, where the savings concentrate.
  • Persist the configuration cache and the Gradle user home across runs, not just locally.
  • Use a runner that keeps a warm pool so the daemon and compiled outputs survive between jobs.
  • Audit task input and output declarations so high hit rates stay correct, not just fast.
  • Auto-heal flaky integration tests and resolution timeouts so they never force a full re-run.

Recommendations

Make remote build-cache hit rate your headline metric

The remote build cache is the only Gradle acceleration feature that works on a cold, fresh runner, and its hit-rate curve is non-linear, so the move from good to excellent is where the savings live. Instrument the hit rate, watch it the way you watch test coverage, and investigate every regression. A 95 percent hit rate cuts modeled build wall-clock by roughly 71 percent versus cold.

Recover the daemon with a warm pool, not a hand-rolled fleet

Daemon reuse returns nothing on an ephemeral runner because the daemon cannot outlive a single job. A runner that keeps a warm pool restores daemon-class savings without the patching, scaling, and cleanup burden of self-hosted infrastructure. Treat the cold daemon as an infrastructure problem, not a Gradle configuration one.

Persist the configuration cache and Gradle user home

The configuration cache and incremental compilation both pay off only when their state survives across runs, which the default ephemeral runner deletes. Persist the configuration cache and the Gradle user home on a warm shared volume so the configuration phase is replayed and unchanged modules are skipped, rather than recomputed on every job.

Audit cacheability so high hit rates stay correct

A high hit rate on a cache that occasionally serves a stale output is worse than a low one, because it destroys trust in caching entirely. Treat task input and output declarations as first-class, use Gradle build scans and cacheability checks to find misdeclared tasks, and keep the cache correct so the team keeps using it.

Auto-heal transient failures so they never force a full re-run

Flaky integration tests, Testcontainers startup races, and dependency-resolution timeouts force a full, expensive Gradle re-run when they go red. Retrying these transient failures on a fresh environment automatically absorbs them before they trigger a re-run, so the time your warm cache saved is not handed straight back to a registry blip.

Outlook

The defining tension in Gradle CI, that ephemeral runners defeat the incrementality Gradle was designed around, is not going away, but the response to it is maturing. Expect the gap between teams that have restored warm state in CI and teams still running cold ephemeral builds to widen through 2026, because the savings compound: a faster build runs more often, which fills the remote cache more densely, which raises the hit rate, which makes the next build faster still.

The remote build cache will remain the center of gravity, and the operational focus will keep shifting from raw hit rate toward hit-rate quality. As more teams reach high hit rates, the differentiator becomes cache correctness and the discipline of declaring task inputs properly, because a fast cache nobody trusts is a cache nobody uses. The teams that treat cacheability as a first-class engineering concern will run high hit rates safely while others oscillate between caching and disabling it after a stale-output scare.

The durable architectural direction is that Gradle CI increasingly wants to look like the Gradle laptop experience: a warm daemon, a populated cache, an incremental working tree, and instant rebuilds. Delivering that in CI means a runner that persists warm state across jobs, a high-quality remote cache, and automatic recovery for the flaky failures that would otherwise force a full rebuild. The teams that build on that foundation will keep the laptop's speed in CI; the teams that do not will keep paying the cold-everything tax on every job.

Methodology

This report combines published Gradle build-tool documentation and JVM ecosystem data with Latchkey's own analysis of CI runner economics. Build timings and cache-hit curves are modeled on a representative multi-module JVM project and will vary with module count, test weight, and cache locality. Cost figures derive from published GitHub-hosted per-minute rates and Latchkey managed rates. Where a figure is attributed to a named source it reflects that source; where a figure is labeled modeled it is an illustrative estimate intended to show direction and magnitude rather than a precise population value. Figures labeled "modeled" are illustrative estimates derived from public pricing and typical pipeline shapes, not a primary survey; figures attributed to a named source reflect that source. Pricing reflects published rates at time of writing and should be verified against current provider pricing.

Sources

More Latchkey reports

See what you would save with Latchkey managed runners and self-healing. Start free → 30-day trial · No credit card