Skip to content
compiler.dev

Cache Gradle Builds in GitHub Actions

Last updated

To cache Gradle in GitHub Actions, use gradle/actions/setup-gradle. It caches the Gradle User Home (dependencies, wrapper, build cache) and manages keys, cleanup and read-only behaviour for you. A hand-written actions/cache for ~/.gradle/caches works, but misses most of that.

Option 1: setup-gradle

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-java@v5
        with:
          distribution: temurin
          java-version: 21
      - uses: gradle/actions/setup-gradle@v6
      - run: ./gradlew build

That is the whole setup. Run Gradle with the wrapper (./gradlew) after the action, because the action's cache is saved when the job ends. You do not need actions/cache or a key.

What the action caches, per the setup-gradle docs (checked October 2026): by default it uses enhanced caching of the caches subdirectories in Gradle User Home, including downloaded dependencies (modules-2), transforms, generated jars, the Kotlin DSL script cache and the local build cache, plus wrapper distributions. A basic mode caches just ~/.gradle/caches and ~/.gradle/wrapper.

Read-only on branches, writes on main

By default, only jobs on the default branch write the cache. Jobs on other branches read what exists and do not save. This keeps pull requests from filling your 10 GB quota with near-duplicate entries, and it means main must run a Gradle build to populate the cache.

- uses: gradle/actions/setup-gradle@v6
  with:
    cache-read-only: ${{ github.ref != 'refs/heads/main' }}

That expression is the default behaviour, shown so you can change it. For example, make a release branch write too. The docs describe overriding it with cache-read-only, including branch expressions.

Cleanup

cache-cleanup removes unused files from Gradle User Home before the cache is saved. Values are on-success (the default), always and never. It never runs when the cache is read-only or disabled. Keep the default: without it the cache accumulates every version of every dependency your project has ever used.

Encrypting the configuration cache

If you use Gradle's configuration cache, entries can contain secrets. setup-gradle takes a cache-encryption-key (a base64 AES key, which you can generate with openssl rand -base64 16 and store as a secret) so the entries are encrypted in the GitHub cache.

Option 2: explicit actions/cache

- uses: actions/cache@v6
  with:
    path: |
      ~/.gradle/caches
      ~/.gradle/wrapper
    key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/libs.versions.toml') }}
    restore-keys: |
      gradle-${{ runner.os }}-

Use this if you must avoid third-party-hosted actions, though gradle/actions is maintained by the Gradle project. You lose automatic cleanup, branch-aware read-only mode and per-job keys. Without cleanup, restore-keys keep layering new entries on old ones, and the cache grows until eviction.

The Gradle build cache and the remote cache

There are two caches in play:

  • Dependency cache (modules-2): downloaded jars. Both options above handle it.
  • Build cache: task outputs, reused when inputs match. Enable it with org.gradle.caching=true in gradle.properties or --build-cache. setup-gradle stores the local build cache inside the GitHub cache, so a PR can reuse compile and test outputs from main.

For larger teams, a remote build cache node (Develocity or a self-hosted build-cache-node) is shared across jobs and developers and is not limited by GitHub's 10 GB. See Gradle's build cache docs for setup.

Also add org.gradle.parallel=true and check org.gradle.workers.max. Gradle uses the number of CPU cores by default, so a bigger machine helps without any config change. In compiler.dev's measured benchmark, an Angular, Spring Boot (Gradle) and Postgres CI run went from 6m57s on GitHub-hosted runners to 2m18s (see benchmarks).

What to cache and what not to

PathCache?Notes
~/.gradle/cachesYesDependencies and build cache
~/.gradle/wrapperYesThe Gradle distribution; avoids a download per run
.gradle/ in the projectCarefulHolds configuration-cache data; setup-gradle handles it
build/ directoriesNoUse the build cache instead; outputs embed absolute paths
~/.m2SeparateOnly if you publish to or read Maven local

How to verify a cache hit

  1. The setup-gradle step summary in the job page lists cache entries restored and saved, and sizes.
  2. In Gradle output, FROM-CACHE next to a task means its output came from the build cache. UP-TO-DATE means unchanged locally.
  3. Dependency downloads should not appear in --info output on a warm run.
  4. gh cache list --key gradle --limit 20 shows entries and last use.

Common mistakes

  1. No cache populated on main. With read-only branches, nothing is saved until a default-branch build runs.
  2. Running Gradle before the setup action. Always put the action first.
  3. Hashing too little in a manual key. Include version catalogs and wrapper properties.
  4. Caching build/. It is large and mismatched across commits.
  5. Ignoring size. Android and multi-module builds produce multi-GB caches. See cache limits.

FAQ

Do I still need actions/cache with setup-gradle?

No. setup-gradle replaces it for Gradle User Home. Keep actions/cache for other things like Node or Docker.

Does it work with Maven?

setup-gradle is Gradle-only. For Maven, cache ~/.m2/repository with actions/setup-java and cache: maven, or actions/cache keyed on **/pom.xml.

Why is my first PR build slow even with caching?

If main has not populated the cache yet, or the cache was evicted, a PR starts cold. Trigger a build on main.

Can I use the Gradle daemon in CI?

Each fresh job starts a new daemon anyway. setup-gradle sets sensible defaults; you rarely need to change them.

Next steps

Related: speeding up Gradle builds end to end, Docker layer caching if your Gradle build produces images, and how to speed up GitHub Actions. See compiler.dev pricing to try the same build on an 8 vCPU machine.