# Android Gradle build slow in GitHub Actions: Gradle cache, build cache, heap and cores

> An Android Gradle build in GitHub Actions is slow because a fresh VM starts with an empty Gradle User Home, Gradle's build cache is off by default, its heap defaults to 512 MB, and a private repository's ubuntu-latest has 2 CPUs. The fixes in order, from Gradle's and Android's docs, and a machine that keeps ~/.gradle between jobs: Skipbox, from $39 a month with unlimited minutes.

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.

- `~/.gradle` stays on your machine: dependencies, the wrapper's distribution and the local build cache are there for the next job, every branch included
- 8 vCPU and 16 GB for $79 a month, against 2 CPUs and 8 GB on a private `ubuntu-latest`
- 20,000 minutes a month on 4 vCPU: $222 on GitHub, $39 on Skipbox

**GitHub Actions at a flat price, unlimited minutes** Skipbox runs your GitHub Actions jobs on your own dedicated machine: $39 a month, however many minutes you run. Docker layers and dependency caches stay between jobs, so the next job starts right away.

A 4 vCPU runner, 20,000 minutes a month: GitHub $222 → Skipbox $39. You save $183 every month.

Moving takes one line: `runs-on: skipbox`.

[Start my 7-day free trial](https://app.skipbox.dev/sign-in?ref=g-android-gradle-build-slow-github-actions-en)

## 1. Cache Gradle User Home

[setup-gradle](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md) caches the parts of Gradle User Home that help the next build: downloaded dependencies, wrapper distributions and the local build cache.

```yaml
- 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 `main` last 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: basic` is the MIT-licensed one.

## 2. Turn on the build cache and parallel execution

From [Gradle's performance guide](https://docs.gradle.org/current/userguide/performance.html), in `gradle.properties`:

```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 comes `FROM-CACHE`. Across CI jobs that needs the local build cache in Gradle User Home to survive, or a remote build cache.
- **`org.gradle.parallel=true`** runs 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](https://developer.android.com/build/optimize-your-build) 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](https://docs.gradle.org/current/userguide/build_environment.html)). 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:

```properties
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](https://docs.github.com/en/actions/reference/runners/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 `main` saved, 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/caches` stays:** dependencies, transforms and the local build cache. With `org.gradle.caching=true`, a task whose inputs are unchanged comes `FROM-CACHE` on the next job, a pull request's job included.
- **The wrapper's Gradle distribution stays** in `~/.gradle/wrapper`, and so do the JDKs `setup-java` installed and Docker's images and layers.
- **setup-gradle steps aside.** When `~/.gradle/caches` already 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-latest` ships one; on Skipbox, add [android-actions/setup-android](https://github.com/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

```yaml
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`.

1. Sign in to Skipbox with GitHub.
2. Install the Skipbox GitHub App and pick the repositories.
3. Change `runs-on` and 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](https://docs.skipbox.dev/isolation).

## 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 2026-11-13 · https://skipbox.dev/guides/android-gradle-build-slow-github-actions
