Android Gradle build slow in GitHub Actions: Gradle cache, build cache, heap and cores
An Android Gradle build in GitHub Actions is slow for four reasons that stack. Each job is a new VM, so Gradle User Home (~/.gradle: dependencies, the wrapper's Gradle distribution, the local build cache) starts empty. Gradle's build cache is off until you set org.gradle.caching=true. Gradle's heap defaults to 512 MB (-Xmx512m), and Android's docs say to raise it to 4–8 GB when garbage collection takes more than 15% of the build. And a private repository's ubuntu-latest has 2 CPUs and 8 GB. Fix them in that order: cache Gradle User Home with gradle/actions/setup-gradle, turn on the build cache and parallel execution, raise the heap, then give the build more cores and memory. The last two are the machine. A runner that keeps ~/.gradle on its disk between jobs skips the cache restore as well.
Skipbox runs your jobs on your account's own machine, which keeps Gradle User Home, the JDKs and Docker's layers between jobs. Change one line, runs-on: skipbox, for a flat $39 a month with unlimited minutes, or skipbox-8x16 for 8 vCPU and 16 GB.
1. Cache Gradle User Home
setup-gradle caches the parts of Gradle User Home that help the next build: downloaded dependencies, wrapper distributions and the local build cache.
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17
- uses: gradle/actions/setup-gradle@v6
- run: ./gradlew assembleDebug
Three things in its docs decide how much it helps:
- Only the default branch writes. By default, jobs on other branches and pull requests read the cache and don't save it, so a long-lived branch keeps restoring what
mainlast saved. - Every run writes a new entry, per job and OS, and each counts toward GitHub's 10 GB per repository.
- The default cache provider is free for public repositories and in Free Preview for private ones.
cache-provider: basicis the MIT-licensed one.
2. Turn on the build cache and parallel execution
From Gradle's performance guide, in gradle.properties:
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.configuration-cache=true
org.gradle.caching=true. "By default, Gradle does not use the Build Cache." With it on, a task whose inputs haven't changed comesFROM-CACHE. Across CI jobs that needs the local build cache in Gradle User Home to survive, or a remote build cache.org.gradle.parallel=trueruns tasks of different modules at once. It needs cores to run them on.- The configuration cache skips the configuration phase when the build scripts haven't changed. Android's build-speed guide recommends it too.
Android's guide adds two project settings that cut work: non-transitive R classes, and turning off Jetifier when no dependency still needs it.
3. Give Gradle a heap that fits the build
Gradle's default JVM arguments are -Xmx512m -XX:MaxMetaspaceSize=384m (build environment). Android's guide says to raise the heap to 4, 6 or 8 GB if garbage collection takes more than 15% of the build time (the Build Analyzer shows it), and to try the parallel collector:
org.gradle.jvmargs=-Xmx6g -XX:+UseParallelGC
On a private repository's ubuntu-latest, 6 GB of heap is three quarters of the machine's 8 GB, with every other process of the build and the OS still to fit. When the machine runs out, Linux stops the build and the job ends with exit code 137. 16 GB, as on Skipbox M, leaves room.
4. Give the build more cores
With parallel execution on, Gradle runs independent modules' tasks at once. A private repository's ubuntu-latest has 2 CPUs (GitHub-hosted runners). GitHub's larger runners have more, for organizations on GitHub Team or Enterprise Cloud, per minute: 8 cores and 32 GB cost $0.022 a minute.
The daemon on CI
Gradle's guide: on CI, "long-lived agents benefit" from the daemon, "but short-lived ones may not". A GitHub-hosted job is short-lived. So is a Skipbox job for the daemon: every process is stopped after each job, so the next job starts a new one. What carries over is on the disk.
What each fix costs on GitHub's runners
- setup-gradle's cache is a download and an unpack at the start of each job, and an upload at the end of each default-branch job. A pull request reads what
mainsaved, not its own last run. - A remote build cache is another server to run, and every hit crosses the network.
- More cores and memory is a larger runner, on GitHub Team or Enterprise Cloud.
Skipbox keeps Gradle User Home
Each plan comes with your account's own machine, kept between jobs:
~/.gradle/cachesstays: dependencies, transforms and the local build cache. Withorg.gradle.caching=true, a task whose inputs are unchanged comesFROM-CACHEon the next job, a pull request's job included.- The wrapper's Gradle distribution stays in
~/.gradle/wrapper, and so do the JDKssetup-javainstalled and Docker's images and layers. - setup-gradle steps aside. When
~/.gradle/cachesalready exists, setup-gradle by default doesn't overwrite it, and the job summary says "Caching for Gradle actions was disabled due to pre-existing Gradle User Home". The same workflow still runs on GitHub's runners. - No Android SDK in the image. GitHub's
ubuntu-latestships one; on Skipbox, add android-actions/setup-android. - More cores and memory by label, at a flat price. Minutes are unlimited.
At least 7× GitHub's cache room on S. A job picks its size by label:
| Label | Machine | Plan |
|---|---|---|
skipbox | 4 vCPU · 16 GB | S, $39 a month |
skipbox-8x16 | 8 vCPU · 16 GB | M, $79 a month |
skipbox-16x32 | 16 vCPU · 32 GB | L, $149 a month |
At 20,000 minutes a month, GitHub's 4-core runner is $222 against $39 for S, and its 8-core runner $422 against $79 for M. GitHub's figures are after GitHub Team's included usage.
Moving is one line
jobs:
build:
runs-on: skipbox-8x16 # was ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17
- uses: android-actions/setup-android@v4
- uses: gradle/actions/setup-gradle@v6
- run: ./gradlew assembleDebug testDebugUnitTest
skipbox-8x16 runs on an M plan; the trial runs runs-on: skipbox.
- Sign in to Skipbox with GitHub.
- Install the Skipbox GitHub App and pick the repositories.
- Change
runs-onand push.
Security
- The machine is your account's alone. No other company's jobs run on it.
- After every job, the runner's credentials, processes and containers are wiped, and so are the credential files in the home directory. Clean every job gives each job a fresh machine instead.
- Code from people without write access (such as pull requests from forks) runs on a fresh VM that reads your caches but can't change them.
The details are in how jobs are isolated.
Try it on the Android build
The trial is free for 7 days or 3,000 job-minutes, whichever comes first, and takes no card. Move the build job to runs-on: skipbox, run it twice, and put the second run's task outcomes (FROM-CACHE, UP-TO-DATE) next to GitHub's.
FAQ
Why is my Android Gradle build so slow in GitHub Actions?
Four reasons stack. A GitHub-hosted job is a new VM, so Gradle User Home (~/.gradle: dependencies, the wrapper's distribution, transforms, the local build cache) starts empty unless a step restores it. Gradle's build cache is off until org.gradle.caching=true. Gradle's default heap is -Xmx512m, which Android's docs say to raise to 4, 6 or 8 GB when garbage collection takes more than 15% of the build. And a private repository's ubuntu-latest has 2 CPUs and 8 GB. Fix them in that order.
How do I cache Gradle in GitHub Actions?
Use gradle/actions/setup-gradle. It caches the parts of Gradle User Home that help: downloaded dependencies, wrapper distributions and the local build cache. By default only jobs on the default branch write the cache; other branches and pull requests read it. Its default cache provider is free for public repositories and in Free Preview for private ones; cache-provider: basic is the MIT-licensed one. Each entry counts toward GitHub's 10 GB per repository.
Should I turn on the Gradle build cache in CI?
Yes. Gradle's performance guide says that by default Gradle does not use the build cache; set org.gradle.caching=true in gradle.properties. Then a task whose inputs haven't changed comes FROM-CACHE instead of running. It only helps across CI jobs if the local build cache (in Gradle User Home) survives the job, or a remote build cache is set up.
How much memory should Gradle get on a GitHub Actions runner?
Android's build-speed guide suggests org.gradle.jvmargs=-Xmx4g, 6g or 8g when the build spends more than 15% of its time in garbage collection, and the parallel collector. A private repository's ubuntu-latest has 8 GB in all, shared with every other process of the build and the OS, so -Xmx6g there leaves little. On a runner with 16 GB, such as Skipbox's skipbox-8x16, a 6 or 8 GB heap fits.
What does Skipbox keep for an Android build?
Gradle User Home: ~/.gradle/caches (dependencies, transforms and the local build cache) and the wrapper's distribution, plus Docker's layers and the tools setup-java installs. setup-gradle sees the existing Gradle User Home and leaves it in place. The Gradle daemon does not survive: every process is stopped after each job. The image has no Android SDK, so the job installs it with android-actions/setup-android.
How much does it cost to try?
Nothing. The trial is 7 days or 3,000 job-minutes, whichever comes first, with no card. Move the assemble job to runs-on: skipbox, run it twice, and compare the second run's task outcomes (FROM-CACHE, UP-TO-DATE) and time with GitHub's.
Updated November 13, 2026