Gradle: The Gradle Daemon

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Gradle 9.8.1 (released on 7 October 2026), with Gradle 8.14.6, the last 8.x release, mentioned where it differs. Every example marked as run was executed with the 9.8.1 wrapper on JDK 26 (Oracle java version "26") on 2026-10-16, on macOS (Apple silicon), in scratch copies of the tidepool build (a root project with the subprojects tidepool-model, tidepool-engine and tidepool-cli), once with *.gradle.kts files and once with *.gradle files. All daemon examples used a Gradle User Home created for the page (selected with the GRADLE_USER_HOME environment variable), so that --status and --stop only saw the daemons started by these examples and the real ~/.gradle was not touched. Blank lines between the parts of Gradle’s output were dropped in places. Scratch paths are shown as <work> (the directory of the project in question); long lines that Gradle prints are shortened with ... where it is said so; a user-specific path to a JDK is shown as ~/Library/Java/JavaVirtualMachines/.... Gradle 8.14.x was not downloaded, so what is said about it comes from the official release notes and is labelled "not run here". Two things were not run at all and are labelled the same way: downloading a JDK through Daemon JVM auto-provisioning, and running a build with no daemon at all (see below why that is hard to reproduce).

A Gradle build runs inside a Java process that is not the one you start. When you type ./gradlew build, a small client JVM starts, looks for a long-lived daemon JVM that can run the build, starts one if there is none, hands it the request and prints what it sends back. The daemon survives the build and waits for the next one, which is why the second build is much faster than the first. This page explains that machinery, the commands that inspect and stop it, the rules for when a new daemon is started, where its logs go, how to size it, and the Daemon JVM criteria that let a project say which JDK its own build needs. The options of the gradle command that do not concern the daemon are in Command-Line Interface; the org.gradle.* properties in general are in Build Environment & Properties; the files the daemon leaves in the Gradle User Home are described in Directories & Logging. For the other two tools: Ant starts a new JVM for every run and has no daemon (see Ant: Running Ant), and Maven gets the same effect from the separate Maven Daemon, described in Maven: Build Performance, Cache, Parallel Builds & mvnd.

Why the daemon exists

Gradle runs on the JVM and uses several libraries whose initialization is slow, so starting a fresh JVM for each command makes every build pay for it. The official page lists what a long-lived process buys:

  • it caches project information across builds (the compiled build scripts and the plugin classes stay loaded);

  • it avoids the JVM start-up for every build;

  • it benefits from the JIT optimisations of the JVM, which accumulate while the process is alive;

  • it watches the file system, so it already knows what changed when the next build starts;

  • it keeps in-memory caches of data such as the hashes of task inputs and outputs that incremental builds need.

The official page says the daemon can reduce build times by 15 to 75 percent when you build the same project repeatedly. The history is short: before Gradle 3.0 (2016-08-15) the daemon had to be switched on explicitly (--daemon, or org.gradle.daemon=true); the 3.0 release notes announce that it is "enabled by default", together with the --status command. The Tooling API that IDEs use always executes builds in a daemon, so a build started from an IDE already uses one.

To see the effect, the same trivial command (compileJava of the model subproject, rerun on purpose with --rerun-tasks and quiet output) was timed with /usr/bin/time -p right after ./gradlew --stop, and then twice more with the daemon the first call had started. The last line uses --no-daemon:

$ ./gradlew --rerun-tasks :tidepool-model:compileJava --quiet     (no daemon running)
real 3.07
$ ./gradlew --rerun-tasks :tidepool-model:compileJava -q          (daemon from the first call)
real 0.37
$ ./gradlew --rerun-tasks :tidepool-model:compileJava -q
real 0.38
$ ./gradlew --rerun-tasks :tidepool-model:compileJava --no-daemon
real 3.97

These are single measurements on one machine, with a project that compiles four classes: they show the shape (the first call and the --no-daemon call pay for starting a JVM and loading Gradle, the later calls do not), not a benchmark. How to measure properly, and the other speed-ups, are in Performance Profiling & Build Scans.

The client and the daemon

Two JVMs take part in every command, and they may run different Java versions. The table summarises the official description:

Process Role Which JVM it uses

Gradle client JVM

Starts when you run gradle or ./gradlew and stays alive for the whole invocation. It finds or starts a daemon, sends the build request (command line, project directory, environment variables) and streams the output back to the console.

The JVM that launches the script: JAVA_HOME, java on the PATH, or the IDE’s. GRADLE_OPTS configures this JVM.

Gradle daemon JVM

Long-lived. Resolves dependencies, evaluates the build scripts, creates and runs the tasks, and keeps the caches between builds.

org.gradle.java.home, a request of the Tooling API (an IDE), or the Daemon JVM criteria (below). org.gradle.jvmargs configures this JVM.

Client and daemon talk over a local socket. The daemon is not a service in the operating-system sense: nothing starts it at boot, and it exits by itself when it has been idle for three hours or when the machine is short of memory.

flowchart TD A["./gradlew task (client JVM)"] --> B{"Idle daemon with the same Gradle version, JVM and immutable options?"} B -- yes --> C["Send the build request to that daemon"] B -- no --> D["Start a new daemon (or a single-use one)"] D --> C C --> E["Daemon runs the build and streams the output to the client"] E --> F["Daemon stays idle for the next build until the idle timeout"]

The distinction explains most surprises. A new JAVA_HOME changes the client but may not change the daemon, and org.gradle.jvmargs changes the daemon but not the client. The two sides are configured in different places, as the table above says, and the page Build Environment & Properties lists every property and variable. ./gradlew --version shows both: the line Launcher JVM is the client, the line Daemon JVM is the daemon (and says where its choice came from).

Looking at the daemons: --status and --stop

--status lists the daemons that this Gradle version can see in the Gradle User Home, and --stop stops them. Both commands work for the current Gradle version only: a daemon started by another Gradle release is neither listed nor stopped. Run in the scratch build, after an earlier build had started a daemon:

./gradlew --status
./gradlew --stop
./gradlew --status
$ ./gradlew --status
   PID STATUS   INFO
 48947 IDLE     9.8.1

Only Daemons for the current Gradle version are displayed. For more on this, please refer to https://docs.gradle.org/9.8.1/userguide/gradle_daemon.html#sec:status in the Gradle documentation.

$ ./gradlew --stop
Stopping Daemon(s)
1 Daemon stopped

$ ./gradlew --status
No Gradle daemons are running.
   PID STATUS   INFO
 48947 STOPPED  (stop command received)

Only Daemons for the current Gradle version are displayed. For more on this, please refer to https://docs.gradle.org/9.8.1/userguide/gradle_daemon.html#sec:status in the Gradle documentation.

The INFO column of a live daemon shows the Gradle version; the STATUS is IDLE, BUSY or, for recently stopped daemons, STOPPED with the reason ("stop command received" here). Stopped entries are kept for a while so that you can tell why a daemon went away, which was exactly the question the 3.0 release notes said had no easy answer before. The same list is behind the message you see when a build starts a new daemon (below). To find daemons of every Gradle version, and processes that the registry has lost, use the JDK tool jps: live daemons are listed under the name GradleDaemon. jps was not captured here because the machine also ran other Gradle daemons that do not belong to these examples. ./gradlew --stop is also the first thing to try when a build behaves strangely after you changed the environment, because the next build starts a clean JVM.

When a build finds no daemon it can use it says so on the first line. After the --stop above, the next build printed:

$ ./gradlew :tidepool-model:compileJava --rerun-tasks
Starting a Gradle Daemon, 2 stopped Daemons could not be reused, use --status for details
> Task :tidepool-model:compileJava

BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.8.1/userguide/configuration_cache_enabling.html

The count of "stopped" daemons is the number of entries that --status lists as STOPPED: they are in the registry but cannot be reused. The same line appears, with different counts, whenever a daemon has to be started.

When a new daemon starts: compatibility

A daemon is reused only if it matches the build request exactly. The official page lists what is compared:

  • the Java version of the daemon’s JVM, and its other JVM attributes;

  • the JVM options and system properties that cannot change after the JVM has started;

  • the Gradle version (a daemon of 9.7.1 is never used by a 9.8.1 build).

The immutable JVM system properties are file.encoding, user.language, user.country, user.variant, java.io.tmpdir, the javax.net.ssl.keyStore* and javax.net.ssl.trustStore* family and com.sun.management.jmxremote. The immutable JVM attributes are the maximum heap (-Xmx), the minimum heap (-Xms), the boot class path (-Xbootclasspath) and the assertion status (-ea). If a request differs in any of these, an existing daemon cannot be used and a new one is started next to it. The idle old daemon stays alive, and uses memory, until it times out or you stop it, which is the usual reason for "my machine is full of Gradle processes": several projects, several JDKs, or IDE and command line with different settings.

To see it, a daemon was started with the default options and gradle.properties was then changed to org.gradle.jvmargs=-Xmx768m:

$ echo "org.gradle.jvmargs=-Xmx768m" > gradle.properties && ./gradlew :tidepool-model:compileJava
Starting a Gradle Daemon, 1 incompatible and 3 stopped Daemons could not be reused, use --status for details
> Task :tidepool-model:compileJava UP-TO-DATE

BUILD SUCCESSFUL in 4s
1 actionable task: 1 up-to-date
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.8.1/userguide/configuration_cache_enabling.html
$ ./gradlew --status
   PID STATUS   INFO
 52163 IDLE     9.8.1
 52269 IDLE     9.8.1
 48947 STOPPED  (stop command received)
 50967 STOPPED  (stop command received)
 51542 STOPPED  (stop command received)

Only Daemons for the current Gradle version are displayed. ...
$ ./gradlew :tidepool-model:compileJava --rerun-tasks
> Task :tidepool-model:compileJava

BUILD SUCCESSFUL in 865ms
1 actionable task: 1 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.8.1/userguide/configuration_cache_enabling.html

The first build started daemon 52269 because 52163 had the other heap size ("1 incompatible"); two idle daemons now coexist, and the third build found the one with -Xmx768m and printed no start message at all. (The ... replaces the long documentation sentence shown in the earlier --status output.)

Since Gradle 9.5 the reason is available: when Gradle finds a daemon but cannot use it, it logs the specific reason at the INFO level. With the heap changed once more (-Xmx640m in the running daemon, -Xmx700m requested) and --info, 9.8.1 printed:

$ ./gradlew help --info
Found daemon DaemonInfo{pid=70145, address=[... port:49960, addresses:[/127.0.0.1]], state=Idle, ..., daemonOpts=-Xmx640m,-Dfile.encoding=UTF-8,...]} however its context does not match the desired criteria.
At least one daemon option is different.
Wanted: DaemonRequestContext{jvmCriteria=/Library/Java/JavaVirtualMachines/jdk-26.jdk/Contents/Home (no Daemon JVM specified, using current Java home), daemonOpts=[-Xmx700m, -Dfile.encoding=UTF-8, ...], applyInstrumentationAgent=true, nativeServicesMode=ENABLED, priority=NORMAL}
Actual: DefaultDaemonContext[uid=...,javaHome=/Library/Java/JavaVirtualMachines/jdk-26.jdk/Contents/Home,javaVersion=26,javaVendor=Oracle Corporation,...,daemonOpts=-Xmx640m,-Dfile.encoding=UTF-8,...]

Long lines are shortened with .... In Gradle 8.14 the log only said that a new daemon would be used (not run here; this is what the 9.5.0 release notes say changed). The Wanted: and Actual: lines show the JVM that the build asked for and the one the daemon runs, which makes the classic "two daemons because the IDE and the shell use different JDKs" case easy to diagnose. The official troubleshooting section describes the IDE form of the same problem: a warning that "Multiple Gradle daemons might be spawned because the Gradle JDK and JAVA_HOME locations are different" (not run here); the cure is to make JAVA_HOME, the IDE’s Gradle JDK and any toolchain in the build agree, or to set the Daemon JVM criteria described below, which every client then follows.

Disabling the daemon: --no-daemon and org.gradle.daemon

The daemon can be switched off for one build, one project, one user or one environment. --daemon switches it on again for one build and overrides any setting that disables it.

How Where

./gradlew build --no-daemon

One build. Overrides settings that enable the daemon.

org.gradle.daemon=false

gradle.properties in the project root (all builds of the project), in the Gradle User Home (the current user) or -Dorg.gradle.daemon=false in GRADLE_OPTS (the environment).

./gradlew build --daemon

One build. Overrides settings that disable the daemon.

"No daemon" does not always mean that no second JVM is used. The official page distinguishes two cases: if the JVM options of the client (GRADLE_OPTS, JAVA_OPTS) do not match what the build requires (org.gradle.jvmargs, the Java home, the Daemon JVM criteria), Gradle forks a single-use daemon that runs the build and is stopped at the end; only if they match exactly does the build run in the client JVM itself. The run below used --no-daemon in the scratch build, whose daemon options are the defaults, and the client JVM does not have them:

$ ./gradlew --rerun-tasks :tidepool-model:compileJava --no-daemon
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/9.8.1/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
Daemon will be stopped at the end of the build
> Task :tidepool-model:compileJava
BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.8.1/userguide/configuration_cache_enabling.html

A build that really runs in the client JVM was not produced here (not run here): it needs GRADLE_OPTS to repeat every daemon argument that Gradle derives, including the locale and encoding properties of the machine, and an attempt with only -Xmx512m in both places still forked a single-use daemon. That is the practical lesson: on a CI machine --no-daemon is a way to get a fresh JVM for each build, not a way to avoid a second JVM.

The official page recommends the daemon for developer machines and for CI servers alike. Disable it where the machine is a throw-away container and the build runs once, or when you are debugging a problem that you suspect is caused by state kept in the daemon.

Daemon logs and the idle timeout

Each daemon writes a log file named daemon-<pid>.out.log in daemon/<gradle-version>/ under the Gradle User Home, next to the registry (registry.bin) that --status reads. The client announces the file in the --info output: the line starts with "The client will now receive all logging from the daemon (pid: …​)" and gives the path. In the scratch Gradle User Home after the examples above:

$ ls daemon/9.8.1
daemon-48947.out.log
daemon-50967.out.log
daemon-51102.out.log
...
registry.bin
registry.bin.lock

The log is at DEBUG level and records the life of the daemon: its start, each build request, when it is marked busy or idle, and why it stopped. Its first lines also record the JVM options the daemon assumed it was started with. To see the life cycle of a daemon with a short idle timeout (next paragraph), these five lines were selected with grep from its log (the file contains many more, including the network interfaces of the machine):

2026-10-16T17:17:31.392+0200 [LIFECYCLE] [org.gradle.launcher.daemon.server.Daemon] Daemon server started.
2026-10-16T17:17:31.407+0200 [INFO] [org.gradle.launcher.daemon.server.DaemonRegistryUpdater] Marking the daemon as busy, address: [4dad4930-2cfd-4863-a44a-7d75e011948b port:65085, addresses:[localhost/127.0.0.1]]
2026-10-16T17:17:32.735+0200 [INFO] [org.gradle.launcher.daemon.server.DaemonRegistryUpdater] Marking the daemon as idle, address: [4dad4930-2cfd-4863-a44a-7d75e011948b port:65085, addresses:[localhost/127.0.0.1]]
2026-10-16T17:17:41.401+0200 [LIFECYCLE] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] Daemon will be stopped at the end of the build after being idle for 0 minutes
2026-10-16T17:17:41.402+0200 [DEBUG] [org.gradle.launcher.daemon.server.DaemonStateCoordinator] Marking daemon stopped due to after being idle for 0 minutes. The daemon is not running a build

The log files are deleted by Gradle’s periodic cache cleanup when they are older than 14 days, and that retention is configurable like the other cache retentions (see Directories & Logging). Look at the log first when a daemon "disappears" or a build hangs: it shows whether the daemon was told to stop, ran out of heap or was expired.

A daemon stops itself after it has been idle for three hours (org.gradle.daemon.idletimeout, in milliseconds; the default is 10800000). The log above comes from a daemon of the Groovy copy started with a project gradle.properties containing org.gradle.daemon.idletimeout=5000: the idle check ran about nine seconds after the build (the log lines are above), and 25 seconds later --status found no live daemon:

$ cat gradle.properties
org.gradle.daemon.idletimeout=5000
$ ./gradlew help -q
...
$ ./gradlew --status
   PID STATUS   INFO
 56588 IDLE     9.8.1
 48947 STOPPED  (stop command received)
$ sleep 25; ./gradlew --status
No Gradle daemons are running.
   PID STATUS   INFO
 48947 STOPPED  (stop command received)
 50967 STOPPED  (stop command received)
 51542 STOPPED  (stop command received)

The log line reads "idle for 0 minutes" because the timeout was shorter than a minute. In production leave the default; on a small CI agent with many projects a shorter value (for example one hour) returns memory sooner.

Memory and JVM arguments of the daemon

If the build does not set a maximum heap, the daemon gets up to 512 MB, which the official page calls adequate for most builds; larger builds, with hundreds of subprojects, may need more. The setting is org.gradle.jvmargs (in gradle.properties, in the project or in the Gradle User Home; command line: -Dorg.gradle.jvmargs=...). To look at what a daemon actually runs with, the page uses a task that prints facts about the JVM it executes in:

Kotlin DSL
// build.gradle.kts (root project)
tasks.register("daemonInfo") {
    description = "Prints facts about the JVM that runs the build."
    doLast {
        val rt = Runtime.getRuntime()
        println("daemon pid:   ${ProcessHandle.current().pid()}")
        println("java.version: ${System.getProperty("java.version")}")
        println("max heap:     ${rt.maxMemory() / (1024 * 1024)} MiB")
        println("input args:   ${java.lang.management.ManagementFactory.getRuntimeMXBean().inputArguments.filter { it.startsWith("-Xm") || it.startsWith("-XX:Max") }}")
    }
}
Groovy DSL
// build.gradle (root project)
tasks.register('daemonInfo') {
    description = 'Prints facts about the JVM that runs the build.'
    doLast {
        def rt = Runtime.runtime
        println "daemon pid:   ${ProcessHandle.current().pid()}"
        println "java.version: ${System.getProperty('java.version')}"
        println "max heap:     ${rt.maxMemory().intdiv(1024 * 1024)} MiB"
        println "input args:   ${java.lang.management.ManagementFactory.runtimeMXBean.inputArguments.findAll { it.startsWith('-Xm') || it.startsWith('-XX:Max') }}"
    }
}

The task only reads the JVM and nothing else, so it is compatible with the configuration cache; both versions were run with --configuration-cache (the first call stores the entry, later calls reuse it) and printed the same facts. The Kotlin DSL run, first with no org.gradle.jvmargs, then with two different values:

$ ./gradlew daemonInfo --configuration-cache -q
daemon pid:   52163
java.version: 26
max heap:     512 MiB
input args:   [-XX:MaxMetaspaceSize=384m, -Xms256m, -Xmx512m]
# gradle.properties: org.gradle.jvmargs=-Xmx768m
daemon pid:   52269
java.version: 26
max heap:     768 MiB
input args:   [-Xmx768m]
# gradle.properties: org.gradle.jvmargs=-Xmx1g -XX:MaxMetaspaceSize=512m
daemon pid:   53143
java.version: 26
max heap:     1024 MiB
input args:   [-XX:MaxMetaspaceSize=512m, -Xmx1g]

Three observations. The default is a 512 MB maximum heap, a 384 MB metaspace limit and (observed here) an initial heap of 256 MB. Setting org.gradle.jvmargs replaces the whole default: with only -Xmx768m the metaspace limit and the initial heap of the default disappeared, so list every argument you still want. And each different combination is a different daemon (three PIDs above), as the compatibility section says. The official page documents the default as "-Xmx512m" with "-XX:MaxMetaspaceSize=384m"; the -Xms256m is not documented there.

Where the arguments come from matters for the other JVMs too. GRADLE_OPTS configures the client JVM and, as the official page notes, normally not the daemon; the recommended way to pass options to the daemon through the environment is GRADLE_OPTS=-Dorg.gradle.jvmargs=.... The official page also names JAVA_OPTS among the client-side variables that must repeat the daemon arguments if a build is to run in-process (see the next section). Worker processes (test JVMs, the Kotlin compiler daemon, worker-API processes) are separate JVMs with their own settings, covered where they are used (see Worker API & Build Services and Testing JVM Projects). A larger daemon heap does not help a test JVM that needs more memory.

Gradle watches heap usage of the daemon to detect memory leaks: when a leak exhausts the heap, the daemon finishes the build that is running and restarts before the next one. It is on by default and is turned off with org.gradle.daemon.performance.enable-monitoring=false (not run here).

Choosing the JVM of the daemon: the Daemon JVM criteria

By default the daemon runs on the same JVM that started the client. That is convenient and wrong for a team: two developers with different JAVA_HOME get different daemons, and a Gradle 9 build on a machine whose default Java is 11 simply cannot start. Since Gradle 9.0 the daemon needs Java 17 or later, while the wrapper and the command-line launcher can still run on Java 8 and then start the daemon on a newer JVM (a build can still target older Java versions through toolchains). Three mechanisms choose the daemon’s JVM, from the strongest to the weakest:

  1. the Daemon JVM criteria in gradle/gradle-daemon-jvm.properties (declarative: a language version, optionally a vendor);

  2. the org.gradle.java.home property (a path), or a request made through the Tooling API, as an IDE does;

  3. the JVM that launched the client (JAVA_HOME, then the java on the PATH).

The criteria feature was introduced as an incubating preview in Gradle 8.8 (it was called the daemon JVM configurable via toolchain in the release notes) and promoted to stable in Gradle 9.2.0 (2025-10-29), when it stopped printing the incubation warning. In 8.14 it was therefore still incubating (not run here). Everything below ran on 9.8.1.

Writing the criteria with updateDaemonJvm

The file is generated, like the wrapper’s properties, by a task in the root project. The command line is ./gradlew updateDaemonJvm --jvm-version=21, and the same can be set in the build script. The task needs toolchain download repositories because by default it also writes a download URL per platform; without them the first attempt failed:

$ ./gradlew updateDaemonJvm --jvm-version=21
> Task :updateDaemonJvm FAILED

[Incubating] Problems report is available at: file://<work>/build/reports/problems/problems-report.html

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':updateDaemonJvm'.
> Error while evaluating property 'toolchainDownloadUrls' of task ':updateDaemonJvm'.
   > Failed to calculate the value of task ':updateDaemonJvm' property 'toolchainDownloadUrls'.
      > Invalid task configuration
          Toolchain download repositories have not been configured.
            For more information, please refer to https://docs.gradle.org/9.8.1/userguide/toolchains.html#sub:download_repositories.
            Possible solution: Configure toolchain download repositories in your build settings.

* Try:
> Run with --scan to get full insights from a Build Scan (powered by Develocity).

BUILD FAILED in 345ms
1 actionable task: 1 executed

(The <work> here is the project directory.) The error is the real one and has two ways out. If you only want to pin the JVM and have no download repository (an air-gapped CI, or a team whose machines all have the JDK installed), configure the task to generate no URLs by giving it no platforms:

Kotlin DSL
// build.gradle.kts (root project)
tasks.named<UpdateDaemonJvm>("updateDaemonJvm") {
    languageVersion = JavaLanguageVersion.of(21)
    toolchainPlatforms = emptyList()
}
Groovy DSL
// build.gradle (root project)
tasks.named('updateDaemonJvm') {
    languageVersion = JavaLanguageVersion.of(21)
    toolchainPlatforms = []
}

Both ran, and wrote the same file. (The official page shows toolchainDownloadUrls.empty() for the Kotlin DSL, which was not used; toolchainPlatforms was what ran.)

$ ./gradlew updateDaemonJvm
> Task :updateDaemonJvm

BUILD SUCCESSFUL in 631ms
1 actionable task: 1 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.8.1/userguide/configuration_cache_enabling.html

$ cat gradle/gradle-daemon-jvm.properties
#This file is generated by :updateDaemonJvm
toolchainVersion=21

Commit this file, like gradle-wrapper.properties, so that every developer and every CI job starts the same JVM. The next build, run with --configuration-cache, started a new daemon and ran on the JDK 21 that happens to be installed on the machine (a JetBrains Runtime), although the client still ran on JDK 26. Because the JVM changed, the configuration cache entry could not be reused, and --version states the source of the choice:

$ ./gradlew daemonInfo --configuration-cache
Starting a Gradle Daemon, 3 incompatible and 3 stopped Daemons could not be reused, use --status for details
Calculating task graph as configuration cache cannot be reused because JVM has changed.

> Task :daemonInfo
daemon pid:   53804
java.version: 21.0.10
max heap:     512 MiB
input args:   [-XX:MaxMetaspaceSize=384m, -Xms256m, -Xmx512m]

BUILD SUCCESSFUL in 6s
1 actionable task: 1 executed
Configuration cache entry stored.

$ ./gradlew --version | grep JVM
Launcher JVM:  26 (Oracle Corporation 26+35-2893)
Daemon JVM:    Compatible with Java 21, any vendor, nativeImageCapable=false (from gradle/gradle-daemon-jvm.properties)

The Groovy DSL copy behaved the same way (daemon on 21.0.10, "configuration cache cannot be reused because JVM has changed", entry stored).

Vendors, native images and what happens when no JDK matches

A vendor can be added with --jvm-vendor=<name> or the vendor property of the task, using the same names as the toolchain spec (the known ones include adoptium, amazon, azul, bellsoft, graalvm, ibm, jetbrains, microsoft, oracle and sap; matching is case-insensitive and an unknown name must match the vendor string exactly). --native-image-capable asks for a JDK that can build native images (not run here). The vendor makes the criteria stricter, and on a machine whose JDK 17 comes from another vendor it makes the build fail. The two JDKs 21 and 17 installed here are both JetBrains Runtimes, and the JDK 26 is Oracle’s. First a mismatch, then a match (the file has to be removed first because the updateDaemonJvm task itself needs a daemon that satisfies the existing file):

$ ./gradlew updateDaemonJvm --jvm-version=17 --jvm-vendor=oracle -q
$ cat gradle/gradle-daemon-jvm.properties
#This file is generated by :updateDaemonJvm
toolchainVendor=ORACLE
toolchainVersion=17
$ ./gradlew daemonInfo -q

FAILURE: Build failed with an exception.

* What went wrong:
Unable to download toolchain matching the requirements ({languageVersion=17, vendor=Oracle, implementation=vendor-specific, nativeImageCapable=false}) from 'null', due to: No defined toolchain download url for MAC_OS on aarch64 architecture.

* Try:
> Learn more about toolchain auto-detection and auto-provisioning at https://docs.gradle.org/9.8.1/userguide/toolchains.html#sec:auto_detection.
> Learn more about toolchain repositories at https://docs.gradle.org/9.8.1/userguide/toolchains.html#sub:download_repositories.
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights from a Build Scan (powered by Develocity).
> Get more help at https://help.gradle.org.

$ rm gradle/gradle-daemon-jvm.properties
$ ./gradlew updateDaemonJvm --jvm-version=17 --jvm-vendor=jetbrains
> Task :updateDaemonJvm

BUILD SUCCESSFUL in 285ms
1 actionable task: 1 executed
Consider enabling configuration cache to speed up this build: https://docs.gradle.org/9.8.1/userguide/configuration_cache_enabling.html
$ cat gradle/gradle-daemon-jvm.properties
#This file is generated by :updateDaemonJvm
toolchainVendor=JETBRAINS
toolchainVersion=17
$ ./gradlew daemonInfo -q
daemon pid:   55020
java.version: 17.0.14
max heap:     512 MiB
input args:   [-XX:MaxMetaspaceSize=384m, -Xms256m, -Xmx512m]
$ ./gradlew --version | grep 'Daemon JVM'
Daemon JVM:    Compatible with Java 17, JetBrains, nativeImageCapable=false (from gradle/gradle-daemon-jvm.properties)

The first failure is the message to recognise: Gradle looked for an installed JDK 17 from Oracle, found none, and, being unable to download one (the file has no download URL for this platform), stopped before running any task. The same message appears for a version that is not installed (a file with toolchainVersion=11 produced the same text, with languageVersion=11, vendor=any vendor). The criteria were also checked against org.gradle.java.home: with the JDK 17 criteria file present, a gradle.properties containing org.gradle.java.home pointing to the JDK 26 was ignored, and the daemon still ran on JDK 17 (Daemon JVM: Compatible with Java 17, JetBrains, ...). Without a criteria file, org.gradle.java.home pointing to the JDK 21 was honoured and --version printed Daemon JVM: ~/Library/Java/JavaVirtualMachines/jbr-21.0.10/Contents/Home (from org.gradle.java.home) (the first part of that path is shortened). So the precedence is the one the official page states: the criteria win over JAVA_HOME and org.gradle.java.home.

Auto-detection and auto-provisioning

Gradle finds the daemon JVM with the same auto-detection as project toolchains: it looks at the JDKs installed on the machine (how it finds them is described on the page about Toolchains). When none matches, auto-provisioning is simpler than for project toolchains: Gradle only looks for a URL for the current platform in gradle-daemon-jvm.properties (the keys toolchainUrl.<OS>.<ARCH>) and downloads the JDK from it into jdks/ in the Gradle User Home. Both org.gradle.java.installations.auto-detect=false and org.gradle.java.installations.auto-download=false apply to the daemon too.

The URLs come from the toolchain download repositories configured in settings.gradle(.kts). The usual one is the Foojay resolver convention plugin. With plugin org.gradle.toolchains.foojay-resolver-convention version 1.0.0 (the latest on the Gradle Plugin Portal on this date) added to settings.gradle.kts, the task wrote a file with one URL per platform and vendor. This only asks the Foojay API for download links: no JDK was downloaded, the file was not used by a build that needed it, and so JDK auto-provisioning itself is not run here.

Kotlin DSL
// settings.gradle.kts
plugins {
    id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}

rootProject.name = "tidepool"
// ...
Groovy DSL
// settings.gradle
plugins {
    id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}

rootProject.name = 'tidepool'
// ...
$ ./gradlew updateDaemonJvm --jvm-version=21 --jvm-vendor=jetbrains
$ cat gradle/gradle-daemon-jvm.properties
#This file is generated by :updateDaemonJvm
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect
toolchainVendor=JETBRAINS
toolchainVersion=21

Two things follow from that file. It lists every platform the team might use (FreeBSD, Linux, macOS, the generic UNIX key and Windows, each for the architectures that apply), controlled by the toolchainPlatforms property of the task (a list of BuildPlatformFactory.of(Architecture, OperatingSystem) values, with the full class names org.gradle.platform.Architecture and org.gradle.platform.OperatingSystem) or replaced with explicit toolchainDownloadUrls for a repository manager of your own; and the file contains links to a third-party service that you now trust to deliver a JDK to every build machine, a supply-chain decision (compare the "trust" discussion in Supply-Chain Security). Teams that cannot allow downloads in CI set org.gradle.java.installations.auto-download=false there and install the JDKs with their image build.

Tools, CI and troubleshooting

  • IDEs. The Tooling API always uses a daemon, so IDE builds use one without any setting. The IDE can choose a JVM for it (in IntelliJ IDEA the "Gradle JDK"), which is why an IDE and a shell can start two daemons for one project; a Daemon JVM criteria file makes both agree. See CI & IDE Integration and Init Scripts & the Tooling API.

  • CI. The official page recommends the daemon on CI too. On a CI agent that runs several builds in a row, keep the daemon (and the Gradle User Home) between jobs; on ephemeral containers the daemon is started once and costs nothing but memory. Always pin the JDK (criteria file or setup-java), so that a job does not start a second daemon because the image changed.

  • Wrong or stale state. Stop the daemons (./gradlew --stop) before assuming a bug in the build, after changing environment variables that the daemon read at start-up, after installing a new JDK, or when the machine is out of memory. The client sends its environment variables to the daemon at every build, so most changes are picked up without a restart; the immutable properties listed above are the exception.

  • Restricted networks. Client and daemon talk over local sockets. Since Gradle 9.5 the environment variable GRADLE_DAEMON_BIND_ADDRESS (for example GRADLE_DAEMON_BIND_ADDRESS=192.168.1.10 ./gradlew build) sets the network address explicitly and skips the auto-detection of the address (taken from the 9.5.0 release notes, not run here).

  • Multiple Gradle versions. Each Gradle version has its own daemons, so a machine that builds with several versions has several sets. --stop only stops the daemons of the version it belongs to.

  • Security. Gradle 9.8.1 contains three high-severity security fixes, among them one about the deserialization used by the daemon and its workers (see Versions, Compatibility & Release History); another reason to keep the wrapper on the latest patch release.

For the general checklist when a build fails or is slow, see Best Practices & Troubleshooting. The tools of the section compare like this:

Ant Maven Gradle

Long-lived build process

None: each ant run starts a JVM

None in Maven itself; the separate Maven Daemon (mvnd) keeps warm JVMs

Built in and on by default since 3.0

Choosing the build JVM

JAVA_HOME or ANT_OPTS

JAVA_HOME, .mvn/jvm.config, MAVEN_OPTS; toolchains for the compile

Daemon JVM criteria file, org.gradle.java.home, JAVA_HOME; toolchains for the compile

Options of the build JVM

ANT_OPTS

MAVEN_OPTS, .mvn/jvm.config

org.gradle.jvmargs (daemon) and GRADLE_OPTS (client)

Where the book is outdated

Gradle in Action (Muschko, Manning 2014, Gradle 1.7) describes the daemon in its chapter 2 as something you opt into with --daemon or org.gradle.daemon=true (and stop with --stop), and notes that it improves start-up. Today the daemon is on by default since Gradle 3.0 (2016), --status exists to inspect it, and the build needs a JVM 17 or later for the daemon since 9.0, while the book’s JDK 1.5 requirement is long gone. The book’s advice to put JVM memory settings in GRADLE_OPTS or JAVA_OPTS is also incomplete: the daemon reads org.gradle.jvmargs, and the Daemon JVM criteria (8.8, stable in 9.2) did not exist.

Summary

  • The gradle command is a client; the build runs in a daemon JVM that stays alive between builds (three hours idle by default), which makes repeat builds much faster. It is on by default since Gradle 3.0.

  • ./gradlew --status lists the daemons of the current Gradle version, ./gradlew --stop stops them; a message at the start of a build says how many daemons were not reusable.

  • A daemon is reused only if the Gradle version, the JVM and the immutable options (heap, encoding and locale properties, …​) are identical; --info explains the mismatch since Gradle 9.5.

  • --no-daemon and org.gradle.daemon=false usually fork a single-use daemon; only an exact match of the client JVM options runs in-process.

  • The daemon log is daemon/<version>/daemon-<pid>.out.log in the Gradle User Home; logs older than 14 days are cleaned up.

  • The default daemon heap is 512 MB; org.gradle.jvmargs replaces the default options as a whole.

  • gradle/gradle-daemon-jvm.properties, written by updateDaemonJvm, pins the JDK of the build itself and takes precedence over org.gradle.java.home and JAVA_HOME; it is stable since 9.2 and needs toolchain download repositories only if URLs are to be generated.