How Do I Cache Gradle Builds in GitHub Actions?
Run the Gradle setup action so Gradle User Home, wrapper distributions and build caches restore on every job. Configuration, cache keys, limits and costs.
Last verified:
Answer
Cache Gradle by adding WarpBuilds/gradle-actions/setup-gradle@v5 after your JDK step, which restores the entire Gradle User Home: wrapper distributions, downloaded dependencies and the Gradle build cache. A workflow that calls only WarpBuilds/setup-java@v5 leaves caching off by default, so every job re-downloads the wrapper and the full dependency graph until you pass a cache input.
That default trips up most Gradle workflows. The Java setup action installs a JDK and stops there. Caching is opt-in through cache: gradle, and even then it covers the package-manager dependency directories rather than the full Gradle User Home. The Gradle setup action is the one that configures Gradle itself and manages the wrapper, dependency and build-cache directories as a unit.
Here is the minimal working configuration on a WarpBuild runner:
name: build
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: warp-ubuntu-latest-x64-8x
steps:
- uses: actions/checkout@v5
- uses: WarpBuilds/setup-java@v5
with:
distribution: temurin
java-version: "21"
- uses: WarpBuilds/gradle-actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
- run: ./gradlew buildNo cache backend configuration appears in that file on purpose. The WarpBuilds/* setup actions bundle the WarpBuild cache client and use WarpBuild Cache automatically when the job runs on a WarpBuild runner, so the only edit against an upstream workflow is the action reference. The full list of forks and their cache inputs is in the setup actions documentation.
For a simpler project that only needs the dependency cache and never touches the Gradle build cache, one action is enough:
- uses: WarpBuilds/setup-java@v5
with:
distribution: temurin
java-version: "21"
cache: gradle
cache-dependency-path: |
**/build.gradle.kts
**/gradle/libs.versions.tomlPlatform scope matters before you commit to either shape. WarpBuild Cache is unavailable on Windows runners, so a Gradle job that depends on cache restores belongs on a Linux or macOS label. The Gradle solution page covers sizing for the JVM heap and worker counts that go with those labels.
Detail
What each action actually caches
The two actions cover different directory sets, and mixing them up is the usual reason a build still spends minutes resolving dependencies after caching is "on".
| Action | Directories covered | Caching default |
|---|---|---|
WarpBuilds/setup-java@v5 with cache: gradle | Gradle dependency directories keyed on the build files you name | Off until cache is set |
WarpBuilds/gradle-actions/setup-gradle@v5 | Gradle User Home: wrapper distributions, downloaded dependencies, build caches | On |
| Neither | Project-local .gradle/ and build/ output directories | Never cached |
The third row is the one that surprises people. The Gradle User Home lives at ~/.gradle and holds everything shared across projects. Project-local state sits inside the repository checkout, and no setup action carries it between jobs. Incremental build state, therefore, resets on every run even with a perfect cache hit.
The pull request configuration
cache-read-only is the input that keeps the cache useful in a busy repository. With it set, a job restores existing entries and writes nothing back.
- uses: WarpBuilds/gradle-actions/setup-gradle@v5
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
gradle-home-cache-excludes: |
caches/build-cache-1Leaving writes on for every branch produces one cache entry per open pull request, and each of those entries carries a copy of the same dependency set. Restricting writes to the default branch gives one authoritative entry that every pull request restores from, which keeps storage flat as the number of open branches grows.
gradle-home-cache-excludes is the release valve for the opposite problem. When the local build cache under caches/build-cache-1 grows faster than it earns back, excluding it keeps the dependency and wrapper caches while dropping the largest and least stable directory.
Two related inputs are worth knowing. cache-write-only seeds an entry without restoring one, which is how you rebuild a poisoned cache from a scheduled job. cache-disabled turns the whole thing off, which is the fastest way to prove whether a failing build is a cache problem.
How the cache key is scoped
WarpBuild Cache is a drop-in replacement for actions/cache@v4, and its scoping rules follow the same model. An entry is addressed by three things: the key, the version and the branch.
The version is the part that catches people. It is a hash over the compression tool used on that runner and the list of cached paths, so entries created under different runner operating systems never match each other. A Gradle User Home cache saved on warp-macos-latest-arm64-6x cannot restore on warp-ubuntu-latest-x64-8x, even though the key string is identical and the dependency set is the same. A matrix build that spans macOS and Linux therefore maintains two entries rather than one, and that is expected behavior rather than a miss to debug.
Branch scoping is the second rule. A branch reads its own entries and the entries of the default branch. A feature branch that has never run cannot read a sibling feature branch's cache, which is another argument for publishing from the default branch only.
Both rules are documented on the caching page, along with the cache-hit output that lets a workflow skip install steps when the restore was exact.
Where Gradle caching still misses
Three costs survive a perfect cache hit, and it is worth knowing them before you go hunting for more cache configuration.
Configuration cache invalidation is the first. Gradle invalidates the configuration cache when build logic inputs change, and that set includes environment variables, system properties, and the files the build script reads at configuration time. A workflow that injects a fresh value on every run, such as a build number or a timestamp, invalidates the configuration cache every time and pays for full configuration on every job. Keeping volatile values out of configuration-time inputs is a build-script fix rather than a caching fix.
Daemon cold starts are the second. Every GitHub Actions job gets a fresh machine, so the Gradle daemon starts cold, and the JVM starts with no JIT profile. A restored Gradle User Home removes downloads and gives back task outputs, and it does nothing for daemon startup or JVM warmup. Repositories where that cost dominates usually get further by shortening the number of separate Gradle invocations per job than by caching more.
Project-local state without a build scan is the third. Because .gradle/ and build/ are never restored, incremental task state resets each run, and without a build scan there is no per-task report telling you which tasks were cache misses. ./gradlew build --info names the tasks it re-ran, while CI observability adds job-level CPU, memory, and duration data.
Snapshot runners are the tool for the state a cache leaves behind, since they boot a job from a saved image of an earlier run's disk rather than restoring named paths into a clean one.
What Gradle caching costs
Cache line items on WarpBuild are published on the pricing page and in the caching documentation, checked on 2026-08-13:
| Cache line item | Hosted runners | BYOC |
|---|---|---|
| Storage | $0.20 per GB-month | Free |
| Write, restore or list operation | $0.0001 per operation | Free |
Runner minutes are the other half of the arithmetic. These are the Ubuntu x64 labels a JVM build usually lands on, with per-minute rates from the pricing page, checked on 2026-08-13:
| Runner label | vCPU | RAM | Price per minute |
|---|---|---|---|
warp-ubuntu-latest-x64-2x | 2 | 8 GB | $0.004 |
warp-ubuntu-latest-x64-4x | 4 | 16 GB | $0.008 |
warp-ubuntu-latest-x64-8x | 8 | 32 GB | $0.016 |
warp-ubuntu-latest-x64-16x | 16 | 64 GB | $0.032 |
warp-ubuntu-latest-x64-32x | 32 | 128 GB | $0.064 |
Now the worked model, with assumptions stated so you can substitute your own. Take a repository with 900 workflow runs per month, a Gradle User Home entry of 2.5 GB, and one restore plus one save per run.
- Storage: 2.5 GB at $0.20 per GB-month is $0.50 per month.
- Operations: 900 runs at 2 operations each is 1,800 operations, at $0.0001 each, or $0.18 per month.
- Cache total: $0.68 per month.
The break-even is how much runner time that $0.68 has to buy back. Divide it by the per-minute rate, then spread the result across the 900 runs:
| Runner label | Price per minute | Monthly minutes the cache must save | Seconds per run |
|---|---|---|---|
warp-ubuntu-latest-x64-2x | $0.004 | 170 | 11.3 |
warp-ubuntu-latest-x64-4x | $0.008 | 85 | 5.7 |
warp-ubuntu-latest-x64-8x | $0.016 | 42.5 | 2.8 |
warp-ubuntu-latest-x64-16x | $0.032 | 21.3 | 1.4 |
warp-ubuntu-latest-x64-32x | $0.064 | 10.6 | 0.7 |
A Gradle dependency resolution that clears a few seconds per run covers the cache bill on any of these sizes, and the threshold falls as the runner gets larger because a minute costs more there. Measure the gap on your own repository by running the same commit with cache-disabled: true and comparing the wall clock of the resolution step.
Expiry
Cache entries expire after 7 days of last use. An entry that every build restores refreshes its last-use timestamp and stays available indefinitely, so the expiry only bites on branches or matrix legs that go quiet. Entries can also be deleted at any time from the job or the console, which is the fix for a cache that captured a broken dependency state.
Related Questions
Does the Java setup action cache Gradle by default?
No. Caching is off by default in WarpBuilds/setup-java@v5 and turns on only when you pass the cache input, for example cache: gradle. For wrapper distributions, downloaded dependencies and the Gradle build cache together, run WarpBuilds/gradle-actions/setup-gradle@v5 after the JDK step instead. The Gradle solution page walks through the full workflow and the runner sizes that suit a JVM build.
Should pull request builds write to the Gradle cache?
Usually not. Set cache-read-only to true on pull request builds so they restore the entry the default branch published and write nothing back, which keeps one authoritative entry per branch instead of one per open pull request. The setup actions documentation lists the rest of the cache inputs, including cache-write-only for seeding.
How long does a Gradle cache entry last on WarpBuild?
Cache entries expire after 7 days of last use, and any entry can be deleted manually from the job or the console at any time. An entry that every build restores keeps refreshing its last-use timestamp and stays available. How long GitHub Actions caches last covers the expiry rules and how they differ from the eviction behavior teams expect.
Does the same setup work for Maven and Android builds?
Yes. WarpBuilds/setup-java@v5 takes maven, gradle or sbt in its cache input, so a Maven build changes one value. Android builds are Gradle builds and use the same setup-gradle configuration on Linux runners. See Maven builds on WarpBuild runners and Android builds on WarpBuild runners for the per-stack details, and the pricing page for the per-minute rate of each label.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.