Gradle Daemon, Workers, Parallelism, File-System Watching, Profiling, and Build Performance: Concepts, Architecture, and Mental Model
Build a performance model that separates startup and configuration cost from execution work, then place Daemon reuse, worker limits, project parallelism, Worker API isolation, file-system watching, memory, and profiling on the correct layer.
Learning objectives
- Separate Gradle client/Daemon JVM identity from Java compilation and test toolchains introduced in Chapter 23.
- Explain what the Daemon reuses, why compatibility matters, and why a warm build must not be compared blindly with a cold build.
- Distinguish worker limits, project parallelism, and Worker API work isolation instead of treating them as one concurrency switch.
- Explain file-system watching as reused file-system knowledge, not a substitute for correct task inputs/outputs.
- Use profiling evidence to separate startup, configuration, dependency resolution, task execution, tests, and packaging before changing settings.
- Recognize memory/GC pressure and CPU/I/O oversubscription as possible reasons that more concurrency makes a build slower.
1. The practical problem: a build can be correct and still waste capacity
Chapter 24 established that Gradle may safely avoid or reuse work only when inputs, outputs, cache keys, and reproducibility are trustworthy. Chapter 25 asks a different question: after correctness is protected, where does wall-clock time go?
Two developers can run the same task graph and see very different elapsed time because one invocation starts a new JVM, one reuses a warmed Daemon, one host has eight effective CPU cores, another CI container has two, one file system can be watched efficiently, and another is a network mount. The task graph can also expose parallel branches—or collapse into one serial critical path.
Performance is a controlled experiment. Do not begin by raising heap, enabling every concurrency flag, or disabling tests/caches/input tracking. Preserve a baseline, change one causal variable, and re-run the same workload with the same source/dependency/toolchain state.
2. Mental model: startup → configuration → execution → evidence
| Layer / control | What it changes | Useful evidence | Common wrong inference |
|---|---|---|---|
| Daemon reuse | JVM startup, JIT warm-up, in-memory Gradle/plugin state across invocations. |
./gradlew --status, Daemon logs, repeated-run
profile/startup time.
|
“Daemon on” means the same Daemon must be reused; JVM/Gradle/immutable JVM settings still have to be compatible. |
| Worker limit | Maximum worker leases Gradle can use across task/Worker API work. |
--max-workers=N, host CPU/RAM notes, wall-clock
comparison.
|
More workers always means faster. CPU, memory, I/O, test forks, and external tools can oversubscribe the machine. |
| Project parallelism | Allows tasks from different projects to execute concurrently when the graph permits. |
--parallel, task timeline/profile, unchanged
dry-run graph.
|
It creates missing task dependencies or makes one long serial chain parallel. |
| Worker API | Lets one task submit units of work with no/classloader/process isolation. | Task/plugin source plus observed worker behavior; process isolation may create session-scoped worker Daemons. | Every Gradle worker is a separate JVM. |
| File-system watching | Reuses file-system knowledge between builds on supported environments. |
--watch-fs --info,
org.gradle.vfs.verbose=true, controlled file
change.
|
It replaces correct task inputs/outputs or guarantees every mounted/remote file system is watchable. |
| Profiling | Measures where time is spent; it is evidence, not an optimization itself. |
--profile HTML, logs, controlled scenario
timings; optional Build Scan.
|
Total CPU percentage identifies the build critical path. |
flowchart TD
W["./gradlew client"] --> D{"Compatible Daemon?"}
D -->|"reuse / start"| C["Initialization + configuration"]
C --> G["Selected task graph"]
G --> P{"Independent work?"}
P -->|projects| PP["--parallel"]
P -->|"within task"| WA["Worker API queue"]
PP --> E["Task execution"]
WA --> E
V["VFS watching"] --> C
V --> E
M["JVM heap / GC + host CPU / RAM"] --> D
M --> E
E --> R["tests + artifacts + profile evidence"]
The Wrapper client is short lived. It connects to or starts a compatible Daemon, which hosts initialization, configuration, dependency resolution, and task execution. The selected task graph determines whether project-level parallelism can overlap branches. A custom task or plugin can independently use the Worker API to submit smaller work items. File-system watching and in-memory Daemon state can reduce repeated-build overhead, but neither changes the correctness contract of the task graph.
3. The Daemon: process reuse, not “a background Gradle install”
Gradle 9.7.1 enables the Daemon by default and currently recommends
it for developer machines and CI servers. The Wrapper client and
Daemon are separate JVM processes. The client is launched by
JAVA_HOME/PATH; the Daemon may be selected
by org.gradle.java.home, Tooling API requests, or
Daemon JVM criteria. A Java toolchain used by javac or
Test is yet another identity.
| Identity | Question to answer | Evidence |
|---|---|---|
| Wrapper/Gradle | Which Gradle version is executing? |
./gradlew --version and reviewed Wrapper
properties.
|
| Client JVM | Which JVM starts the Wrapper client? |
java -version, JAVA_HOME,
environment capture.
|
| Daemon JVM | Which JVM and immutable args run the build engine? |
./gradlew --status, Daemon log,
org.gradle.jvmargs.
|
| Compile/test toolchain | Which JDK compiles/runs project code? |
javaToolchains, task compiler/launcher
configuration from Chapter 23.
|
Gradle only reuses a Daemon when its Gradle version, Java identity, JVM args, and other compatibility requirements match. Changing heap size can therefore start another Daemon rather than “resize” the existing one.
4. Read-only inspection before tuning
./gradlew --version
./gradlew --status
./gradlew projects
./gradlew tasks --all
./gradlew perfPipeline --dry-run # when the lab task exists
java -version
printf 'JAVA_HOME=%s
' "${JAVA_HOME:-<unset>}"
printf 'GRADLE_USER_HOME=%s
' "${GRADLE_USER_HOME:-<default>}"
--status shows Daemons for the same Gradle version, not
every Gradle process on the machine. With a JDK,
jps can provide an additional process view. Preserve
these identities with the timing evidence.
5. Worker limits, project parallelism, and Worker API are different controls
--max-workers=N controls Gradle’s maximum worker
leases; the current default is the number of detected processors.
--parallel enables concurrent execution across
independent projects and is off by default. A plugin/custom task can
separately use Worker API queues to parallelize
work inside one task.
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.gradle.workers.WorkerExecutor
interface DigestParameters : WorkParameters {
val inputFile: RegularFileProperty
val outputFile: RegularFileProperty
}
abstract class DigestWork : WorkAction<DigestParameters> {
override fun execute() {
// Small example only. Real work should be deterministic and fully modeled.
val bytes = parameters.inputFile.get().asFile.readBytes()
parameters.outputFile.get().asFile.writeBytes(bytes)
}
}
abstract class DigestTask @Inject constructor(
private val workers: WorkerExecutor
) : DefaultTask() {
@get:InputFile abstract val inputFile: RegularFileProperty
@get:OutputFile abstract val outputFile: RegularFileProperty
@TaskAction
fun submit() {
val queue = workers.noIsolation()
queue.submit(DigestWork::class.java) {
inputFile.set(this@DigestTask.inputFile)
outputFile.set(this@DigestTask.outputFile)
}
}
}
noIsolation() has the least isolation.
classLoaderIsolation() separates classpaths.
processIsolation() can execute work in separate
session-scoped worker Daemons. That last mode adds process isolation
and startup/memory cost, so it should be justified by library/JVM
isolation needs rather than selected as a generic speed option.
6. File-system watching: reusable file knowledge
On supported operating systems/file systems, Gradle 9.7.1 enables file-system watching by default. The Virtual File System (VFS) can retain file metadata between builds so Gradle does less filesystem work. It does not make an undeclared input safe, and a container bind mount, network filesystem, unusual volume driver, or unsupported platform may behave differently.
./gradlew help --watch-fs --info
./gradlew help --no-watch-fs --info
./gradlew help --watch-fs -Dorg.gradle.vfs.verbose=true --info
Read logs as evidence, not as a brittle string assertion. Gradle 9.7.1 also improved file-system-watching compatibility with custom project-cache directory locations; that does not imply the underlying custom filesystem itself supports every watch behavior.
7. Profiling: measure the stage that is actually slow
| Stage | Typical evidence | Typical causes |
|---|---|---|
| Startup | Profile startup section; Daemon status/log. | Wrapper distribution startup, no compatible Daemon, init scripts, JVM startup. |
| Configuration | Profile project/configuration section. | Eager I/O/computation, cross-project configuration, plugin configuration, disabled Configuration Cache. |
| Resolution | Task/log timing around dependency resolution. | Cold dependency metadata/artifacts, repository latency/order, dynamic versions. |
| Execution | Per-task profile times and task graph. | Compilation/tests/generation/package work, serial dependency chain, external processes. |
| Resource saturation | OS/container CPU/RAM/I/O plus GC/Daemon evidence. | Too many workers/forks, heap pressure, I/O bottleneck, external services. |
./gradlew perfPipeline --profile --console=plain
# Default report location for the root build:
ls -1t build/reports/profile/*.html | head -n 1
--profile is the mandatory free/local path in this
chapter. --scan can provide richer detail but publishes
build metadata to an external service, so it is optional and
requires an explicit privacy/data decision.
8. Memory and GC: capacity is finite
org.gradle.jvmargs controls the Daemon JVM, not the
short-lived client. Current Gradle defaults include a 512 MiB
maximum heap and a 384 MiB maximum metaspace unless the build
environment requests different values. Larger builds may need more
heap, but “increase Xmx” is not a diagnosis.
A host with four cores and 4 GiB available memory can become slower when Gradle workers, test JVMs, process-isolated Worker API jobs, Kotlin daemons, and external compilers all compete. When concurrency rises, observe wall time, CPU utilization, resident memory, swapping, test stability, and Daemon/GC evidence together.
9. DevOps connection: optimize the measured critical path
CI capacity is an economic resource. The useful metric is often feedback latency or total agent-minutes for a change—not “CPU was 90%.” If one ten-minute task lies on every path to completion, making unrelated three-second tasks parallel will not materially reduce the build. If three independent four-minute modules dominate the graph, project parallelism may.
A production optimization therefore carries four proofs: the same input/build identity, a measured before/after, unchanged correctness gates, and an explanation of why the mechanism should generalize to the target developer/CI topology.
Knowledge check
Why can two ./gradlew invocations use different
Daemons?
Daemon reuse requires compatible Gradle/JVM identities and immutable JVM arguments. A different Java home, Gradle version, heap, locale-related JVM property, or other compatibility input can cause a new Daemon.
Does --max-workers=8 automatically enable
project-level parallel execution?
No. It sets a worker limit. Project-level parallel execution is
a separate --parallel/org.gradle.parallel
control, and the task graph must expose independent project
work.
What does process-isolated Worker API work add?
A separate session-scoped worker process/Daemon with stronger process/JVM isolation, at additional startup and memory cost.
What is the minimum free profiling path in this chapter?
Use --profile and inspect the local HTML report
together with controlled run logs/environment notes.
Why is high CPU not enough to prove an optimization?
CPU utilization does not identify the wall-clock critical path or prove correctness. Compare the same workload and preserve task/test/artifact evidence.
What does file-system watching never replace?
Correctly declared task inputs/outputs and controlled build logic. Watching is an optimization for file-state knowledge, not a correctness model.
Official references and version notes
- Gradle 9.7.1 release notes — current pinned patch release, including 9.7.1 file-system-watching improvements.
- Gradle Daemon — client versus Daemon JVM, compatibility, status/logs, CI recommendation, memory defaults, and performance behavior.
-
Build environment configuration
—
org.gradle.jvmargs,org.gradle.parallel,org.gradle.workers.max, and VFS properties/defaults. -
Gradle CLI
—
--max-workers,--parallel,--profile,--scan,--watch-fs, and Daemon options. - Developing Parallel Tasks / Worker API — work queues and no/classloader/process isolation; worker Daemons are scoped to one build session.
-
Inspecting and profiling builds
— Build Scan, free local
--profilereports, and low-level profiling options. - Best practices for performance — measure configuration/execution work and avoid expensive configuration computations.
- Parallel project execution — project-parallel behavior, graph constraints, and the distinction from configuration-on-demand.
- Compatibility matrix — current Gradle runtime and supported-platform expectations, including file-system assumptions.
Version-sensitive behavior was rechecked against current Gradle
primary documentation on 2026-08-24. The mandatory path uses Gradle
9.7.1 through the previously verified Wrapper, JDK 21 as the Gradle
runtime/toolchain, Java 17 as the project target, JUnit 6.1.3 only
for the small local test fixture, and an isolated
GRADLE_USER_HOME. Build Scan publication and
commercial/hosted telemetry are optional; the required evidence uses
the free local --profile report and ordinary logs.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.