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 |
The JVM that launches the script: |
Gradle daemon JVM |
Long-lived. Resolves dependencies, evaluates the build scripts, creates and runs the tasks, and keeps the caches between builds. |
|
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.
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 |
|---|---|
|
One build. Overrides settings that enable the 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:
// 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") }}")
}
}
// 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:
-
the Daemon JVM criteria in
gradle/gradle-daemon-jvm.properties(declarative: a language version, optionally a vendor); -
the
org.gradle.java.homeproperty (a path), or a request made through the Tooling API, as an IDE does; -
the JVM that launched the client (
JAVA_HOME, then thejavaon thePATH).
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:
// build.gradle.kts (root project)
tasks.named<UpdateDaemonJvm>("updateDaemonJvm") {
languageVersion = JavaLanguageVersion.of(21)
toolchainPlatforms = emptyList()
}
// 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.
// settings.gradle.kts
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
rootProject.name = "tidepool"
// ...
// 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 exampleGRADLE_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.
--stoponly 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 |
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 |
|
|
Daemon JVM criteria file, |
Options of the build JVM |
|
|
|
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
gradlecommand 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 --statuslists the daemons of the current Gradle version,./gradlew --stopstops 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;
--infoexplains the mismatch since Gradle 9.5. -
--no-daemonandorg.gradle.daemon=falseusually 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.login the Gradle User Home; logs older than 14 days are cleaned up. -
The default daemon heap is 512 MB;
org.gradle.jvmargsreplaces the default options as a whole. -
gradle/gradle-daemon-jvm.properties, written byupdateDaemonJvm, pins the JDK of the build itself and takes precedence overorg.gradle.java.homeandJAVA_HOME; it is stable since 9.2 and needs toolchain download repositories only if URLs are to be generated.