Skip to content
compiler.dev

Speed Up Gradle in GitHub Actions

Last updated

To speed up Gradle in GitHub Actions, use gradle/actions/setup-gradle for caching, turn on the Gradle build cache, the configuration cache and parallel execution, and fork tests across cores. Gradle is a good fit for caching because it knows which task outputs are still valid, but only if you give it a cache that survives between runs.

This guide shows the settings and a complete workflow. For the general approach, read how to speed up GitHub Actions.

Use setup-gradle, not hand-rolled caching

gradle/actions/setup-gradle is Gradle's own action. Per its docs, caching is on by default using the enhanced provider, and a few behaviors are worth knowing:

  • Only jobs on the default branch write to the cache. Other branches read from it. This protects the cache from PR noise.
  • Daemons are stopped in the post step before the cache is saved.
  • The cache is cleaned up only when all Gradle builds in the job succeed.
  • cache-read-only: true makes a job read but never write, which you may want for jobs that run odd tasks.
  • Configuration-cache data can be saved if you set cache-encryption-key (a base64 AES key, generated with openssl rand -base64 16, stored as a secret).
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
  with:
    distribution: temurin
    java-version: 21
- uses: gradle/actions/setup-gradle@v6
- run: ./gradlew build --scan

setup-java has its own cache: gradle option. Do not enable both; setup-gradle does more and the two can conflict.

gradle.properties for CI

Three settings change build time more than anything else. Put them in gradle.properties (so local builds get them too) or pass them as flags:

org.gradle.caching=true
org.gradle.parallel=true
org.gradle.configuration-cache=true
org.gradle.jvmargs=-Xmx3g -XX:+UseParallelGC
  • Build cache (org.gradle.caching) reuses outputs of tasks whose inputs did not change, such as compiled classes and test results. With setup-gradle this is stored in the GitHub Actions cache. On a cache hit, whole modules show as FROM-CACHE.
  • Parallel (org.gradle.parallel) runs independent subprojects at the same time. It helps multi-module builds and does nothing for a single module.
  • Configuration cache (org.gradle.configuration-cache) stores the result of the configuration phase, so repeat builds skip it. It needs plugins that support it, and some builds fail with violations until fixed. Run ./gradlew build --configuration-cache once and read the report before enabling it permanently. Gradle's docs list plugin compatibility.
  • JVM memory. A default 2 vCPU hosted runner has 7 GB of RAM, so heap size matters. Too small and Gradle spends time in garbage collection, too large and the runner swaps.

Test parallelism

Gradle runs test classes in one JVM by default. Two properties in build.gradle.kts use more cores:

tasks.withType<Test>().configureEach {
    maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
    forkEvery = 0
    useJUnitPlatform()
}

maxParallelForks starts that many test JVMs in parallel for one module. Each fork uses memory and its own database connections, so tests that share a database need isolation per fork (for example a schema or container per fork) or they will collide. With Testcontainers, containers per fork multiply memory use. If you shard tests across jobs instead, Gradle's --tests filter or a plugin that splits by timing works; see test sharding in GitHub Actions.

Avoid doing the work twice

  • build runs everything. If you also run test and check separately, you re-run tasks that were up to date. Gradle skips up-to-date tasks within one invocation, but separate steps on a fresh VM do not share outputs unless the build cache is on.
  • Run once, pass artifacts. Compile in one job with ./gradlew assemble, then run tests in several jobs that restore from the build cache.
  • Skip what you do not need. Use -x javadoc or -x dokkaHtml in PR checks, and generate docs only on release.
  • Wrapper. Always use ./gradlew, so the distribution is pinned. setup-gradle caches the downloaded wrapper distribution.
  • Docker. If you build images from Gradle, layer caching matters more than Gradle settings. See Docker layer caching in CI.

Read the build scan

--scan (or the Develocity plugin) shows task timelines, cache hit and miss reasons, and slow dependency resolution. If a task misses the cache on every run, the scan shows which input changed. Common causes are timestamps embedded in outputs, absolute paths and unpinned dynamic versions like 1.+.

What more CPU cores buy you

Gradle parallelizes projects and tests, and the JVM compiler is multi-threaded, so it benefits more than most stacks. In our benchmarks, an Angular + Spring Boot (Gradle) + Postgres project went from 6m57s on a GitHub-hosted runner to 2m18s on compiler.dev (3.0x). That is our largest speedup, from a full CI run with 10 test shards and a real database, measured 8 October 2026. It also had more memory to work with, 16 GB against 7 GB, which helps JVM builds.

Results vary with how parallel your module graph is. A single big module gains less. To measure, change one line to runs-on: [self-hosted, Linux, compiler-ci]; see pricing and compare. Related: the Gradle template, Maven guide and GitHub Actions caching.

FAQ

Should I use setup-java cache: gradle or setup-gradle?

Use gradle/actions/setup-gradle. It caches more and manages the cache lifecycle. Do not enable both.

Why does only the main branch write the Gradle cache?

It stops PR builds from filling and evicting the shared cache. Branches still read what main saved. You can change it with cache-read-only.

Is the Gradle configuration cache safe to enable?

It is stable in recent Gradle versions, but each plugin has to support it. Try it with --configuration-cache first and fix reported problems.

How many parallel test forks should I use?

Start with half the cores and watch memory. Tests that use a shared database need isolation per fork.

Run this Gradle build on compiler.dev 200 free minutes

Start free