Why Does the Gradle Daemon Start on Every Run?

A GitHub Actions job gets a new virtual machine, so no Gradle daemon survives to reuse. The startup cost breakdown, the settings that cut it, and the numbers.

Answer

The Gradle daemon starts on every GitHub Actions run because there is no daemon left over to connect to: each GitHub-hosted runner is a new virtual machine, and the same is true of a WarpBuild cloud runner, so the first ./gradlew invocation of the job has to start one. That cost repeats on every run unless the machine state itself is carried between runs, because a daemon is a process on one host and no workflow setting can move a process to the next host.

On a laptop the arrangement works the other way. The daemon is a long-lived background process that hosts Gradle's execution engine, it has been enabled by default since Gradle 3.0, and an idle daemon stays alive for 3 hours, so the second build of the afternoon connects to the first build's process and reuses its in-memory caches of plugin classes and task input hashes. A GitHub Actions job never gets a second build on the same machine, so the daemon is started, used once, and thrown away with the runner.

Passing --no-daemon does not remove the cost. Gradle still runs the build in a JVM, either the client JVM when its arguments match what the build asks for or a single-use process that exits at the end, so JVM startup and the configuration phase are still paid. What the flag gives up is the in-memory caching a daemon would have held for a later build.

Three separate costs hide behind the phrase "the daemon starts every run", and they respond to different fixes:

CostWhat happensWhat carries it between runs
Daemon startJVM boot, plugin classloading, no JIT profile yetNothing. A process cannot be restored
Configuration phaseInit scripts, the settings script, build scripts and plugins are evaluated to produce the task graphThe configuration cache entry in PROJECT/.gradle/configuration-cache
Dependency resolutionWrapper distribution and the dependency graph are downloaded into ~/.gradleThe Gradle User Home cache, or a restored runner disk

Only the first row is truly unavoidable. The other two are repeated work that the configuration cache, the build cache and a prepared machine can each remove.

Detail

The configuration phase is the part worth attacking

Gradle's configuration cache turns on with org.gradle.configuration-cache=true in gradle.properties or --configuration-cache on the command line. On a hit, the entire configuration phase is skipped: init scripts, the settings script, build scripts and plugins are not re-evaluated, and Gradle restores the task graph from the entry instead.

The catch on GitHub Actions is where the entry lives. It is written to PROJECT/.gradle/configuration-cache inside the repository checkout, and it is encrypted on disk with a machine-specific key held in the Gradle User Home. A cache action that restores ~/.gradle therefore brings back the dependency graph and leaves the configuration cache behind, which is why a job can report a full dependency cache hit and still spend a minute configuring a multi-module build.

The build cache is the third setting and covers a different phase again. org.gradle.caching=true lets Gradle reuse task outputs whose inputs have not changed, with the local cache stored under the Gradle User Home. Configuration cache, build cache and dependency cache are three separate stores, and a build that enables one and expects the others to follow is the usual source of the "caching is on and nothing got faster" report. The Gradle configuration cache guide covers the entry-reuse rules and the build logic patterns that invalidate an entry on every run.

The configuration that removes the repeated work

# gradle.properties
org.gradle.configuration-cache=true
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=1g
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' }}
          cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}

      - run: ./gradlew build

cache-encryption-key is the input that lets configuration cache data be stored alongside the Gradle User Home; the upstream action documents it as a base64 AES key, generated once with openssl rand -base64 16 and held as a repository secret. cache-read-only keeps pull request builds restoring the entry the default branch published rather than writing one entry per open branch. No cache backend appears in that file because the WarpBuilds/* forks bundle the cache client and use WarpBuild Cache automatically on a WarpBuild runner, as the setup actions documentation describes. The cache inputs and the per-language forks are listed on how to cache Gradle builds in GitHub Actions.

The prepared machine option

When configuration time dominates and the entry keeps missing, the next lever is to stop starting from a clean disk. Snapshot runners boot a job from a saved image of an earlier run's disk, so the checkout, the Gradle User Home and the PROJECT/.gradle/configuration-cache directory all arrive already populated:

    runs-on: >-
      ${{ github.ref == 'refs/heads/main'
        && 'warp-ubuntu-latest-x64-8x;snapshot.enabled=true'
        || 'warp-ubuntu-latest-x64-8x;snapshot.key=gradle-main' }}

Four constraints shape how far this goes. The daemon process still starts from scratch after the boot, so what comes back is disk state. Snapshot boots take 45 to 60 seconds. Snapshots are deleted after 15 days, and /tmp is not preserved. Snapshot labels are honored only on cloud Ubuntu runners, so an Android release build pinned to macOS cannot use them. One more habit matters here: a git clean -ffdx step before the snapshot deletes PROJECT/.gradle/configuration-cache along with the credentials it was meant to remove, so prefer targeted removals of credential paths.

What the repeated work costs

Ubuntu x64 rates from the pricing page, checked on 2026-08-13:

Runner labelvCPURAMPrice per minute
warp-ubuntu-latest-x64-2x28 GB$0.004
warp-ubuntu-latest-x64-4x416 GB$0.008
warp-ubuntu-latest-x64-8x832 GB$0.016
warp-ubuntu-latest-x64-16x1664 GB$0.032
warp-ubuntu-latest-x64-32x32128 GB$0.064

Snapshot line items on the same page, checked the same day: $0.04 per restore and $0.025 per snapshot-hour.

Now a worked model with the assumptions stated, so you can substitute your own. Take a repository with 800 workflow runs per month on warp-ubuntu-latest-x64-8x, and suppose daemon start plus configuration accounts for 40 seconds of each run.

  • Repeated startup time: 800 runs at 40 seconds is 8.9 hours, or 533 minutes.
  • At $0.016 per minute, that is $8.53 per month.
  • One snapshot alias held for a 30 day month is 720 snapshot-hours at $0.025, or $18.00, plus 800 restores at $0.04, or $32.00, for $50.00 per month.

The $8.53 line is what the configuration cache is competing for, and it is close to free to enable. The $50.00 snapshot line has to buy back 3,125 minutes at the 8 vCPU rate, which is 3.9 minutes per run, so a snapshot earns its place on repositories where dependency resolution and a cold local build cache cost minutes rather than seconds.

Measure before choosing. ./gradlew build --profile writes an HTML report under build/reports/profile that separates configuration time from task execution time, which tells you whether the 40 seconds above is 5 seconds or 90 in your repository. Sizing, worker counts and daemon heap for the label you land on are covered on Gradle build caches on GitHub Actions runners.

Does passing --no-daemon make a GitHub Actions build faster?

No. Gradle still runs the build in a JVM, either the client JVM when its arguments match the build requirements or a single-use process that exits at the end, so JVM startup and the configuration phase are still paid. The flag only gives up the in-memory caching a daemon would have held for a second build that never happens on a fresh runner. Gradle build caches on GitHub Actions runners covers the daemon heap and worker settings that do change the number.

Can I keep a Gradle daemon alive between GitHub Actions jobs?

No. A daemon is a process on one machine, and each GitHub-hosted runner is a new virtual machine, so nothing in a workflow file can move a running process to the next job. A snapshot runner brings back the disk that the last run left behind, which covers the Gradle User Home and the configuration cache entry, while the daemon process itself still starts from scratch after the boot.

Does the configuration cache survive between runs on GitHub Actions?

Only if something carries the project directory. Entries live in PROJECT/.gradle/configuration-cache, which the Gradle User Home cache actions never touch, so the two ways to keep an entry are the cache-encryption-key input on setup-gradle or a snapshot runner that restores the whole disk. The Gradle configuration cache guide walks through both, and how to cache Gradle builds in GitHub Actions covers the dependency side.

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.