Gradle Daemon, Workers, Parallelism, File-System Watching, Profiling, and Build Performance: Diagnostics, Failure Modes, Security, and Performance
Diagnose oversubscription, incompatible Daemon JVMs, unsupported file watching, shared-file races, memory pressure, and performance changes that accidentally weaken correctness controls without deleting normal Gradle state.
Learning objectives
- Apply an evidence-preserving performance diagnostic sequence before changing build or cache state.
- Diagnose oversubscription by separating Gradle workers, test forks, Worker API processes, and host limits.
- Recognize incompatible Daemon JVM/argument state and compare it with CI deliberately.
- Treat file-system watching as environment-dependent and preserve a correctness-safe fallback.
- Detect shared-file races exposed by parallel execution and repair the task model instead of disabling evidence.
- Reject “optimizations” that disable tests, declared inputs, dependency verification, or other correctness/security gates.
1. Diagnostic sequence: preserve evidence first
- Preserve concise commands, elapsed time/profile, task outcomes, and the first failure.
- Confirm Wrapper/Gradle version, client JVM, Daemon JVM requirements, Java toolchains, and CI resource limits.
- Inspect settings/build scripts/properties for eager work, worker/parallel/heap/VFS flags, and test forks.
- Inspect the selected task graph and identify the wall-clock critical path.
- Inspect dependency/repository/cache/filesystem state without deleting normal caches.
- Inspect compiler/test/plugin/custom-task/Worker API evidence.
- Apply the least destructive correction to one cause.
- Repeat the same workload and re-prove tests, task graph, and artifact identity.
2. Failure: too many workers make the build slower
Symptom: --parallel --max-workers=8 on
a 2-vCPU/4-GiB runner is slower than workers=2, with high context
switching/GC or swap.
Preserve: both profile reports, exact worker/test-fork settings, runner CPU/memory quota, and failure rate.
./gradlew perfPipeline --rerun-tasks --no-build-cache --no-configuration-cache --parallel --max-workers=8 --profile --console=plain | tee workers8.log
./gradlew perfPipeline --rerun-tasks --no-build-cache --no-configuration-cache --parallel --max-workers=2 --profile --console=plain | tee workers2.log
Repair: choose the lower measured concurrency budget, then separately tune test forks/process-isolated workers if they are the real memory/CPU consumers. Do not “fix” contention by skipping tests.
3. Failure: local Daemon JVM arguments differ from CI
Symptom: local repeated builds are fast, CI repeatedly starts a new/single-use build JVM or has different GC/heap behavior.
./gradlew --version
./gradlew --status
printf '%s
' "${JAVA_HOME:-<unset>}"
printf '%s
' "${GRADLE_OPTS:-<unset>}"
printf '%s
' "${JAVA_OPTS:-<unset>}"
grep -E '^(org[.]gradle[.]jvmargs|org[.]gradle[.]java[.]home|org[.]gradle[.]daemon)' gradle.properties 2>/dev/null || true
ls "$GRADLE_USER_HOME/daemon/9.7.1" 2>/dev/null || true
Gradle Daemon compatibility includes Gradle version, Java
version/home, immutable JVM properties, and heap/JVM args. If CI’s
org.gradle.jvmargs differs, a different Daemon is
expected. Standardize intentionally; do not copy workstation heap
values into a small CI container without capacity evidence.
4. Failure: file watching assumption does not fit the filesystem
Symptom: repeated builds on a bind/network/synchronized mount show unexpected VFS behavior or Gradle reports that watching is unavailable.
./gradlew help --watch-fs --info -Dorg.gradle.vfs.verbose=true --console=plain | tee vfs-on.log
./gradlew help --no-watch-fs --info --console=plain | tee vfs-off.log
Interpretation: unsupported watching is primarily a performance/context finding. If correctness depends on watching, the task model is already wrong. Repair or move to a supported filesystem if measurement justifies it; otherwise allow Gradle’s fallback or explicitly disable watching for that environment.
5. Intentionally broken example: parallel tasks mutate one shared file
The following pattern is unsafe because two project tasks write the same root file without a declared relationship or separate outputs. Serial execution can hide the defect; parallel execution exposes it.
// BROKEN training example in two different subprojects:
tasks.register("appendShared") {
doLast {
rootProject.layout.buildDirectory.file("shared/results.txt")
.get().asFile.appendText("${project.path}\n")
}
}
./gradlew :alpha:appendShared :beta:appendShared --parallel --max-workers=2 --console=plain
cat build/shared/results.txt 2>/dev/null || true
Do not repair by forcing alphabetical order. Give
each producer its own declared
@OutputFile/@OutputDirectory, then add a
separate aggregation task that consumes those outputs. That restores
data ownership and lets Gradle schedule safely.
// Safer shape: each project owns one file under its own build directory.
tasks.register("writeOwnResult") {
val out = layout.buildDirectory.file("results/result.txt")
outputs.file(out)
doLast {
out.get().asFile.apply {
parentFile.mkdirs()
writeText("${project.path}\n")
}
}
}
// Aggregate later from declared producer outputs; do not concurrently append one shared file.
6. Failure: the “performance fix” weakens correctness
| Fast-looking change | Why it is invalid | Correct response |
|---|---|---|
Add -x test to CI default |
Removes verification evidence instead of optimizing it. | Profile tests; fix slow fixtures, right-size forks, or split deliberate suites while keeping required gates. |
Mark changing input @Internal |
Can create stale UP-TO-DATE/cache hits. |
Declare the input accurately; optimize serialization/path sensitivity, not correctness. |
| Disable dependency verification/repository policy | Reduces supply-chain controls, often unrelated to the critical path. | Measure resolution separately; improve trusted repository/cache placement. |
Delete ~/.gradle before every build |
Destroys useful dependency/cache/Daemon evidence and creates artificial cold builds. |
Use isolated fresh GRADLE_USER_HOME only for
controlled comparison.
|
| Increase every heap/fork/worker value | Can increase GC/swap/contention. | Tune one bounded resource at a time against host limits and profile evidence. |
7. Separate configuration bottlenecks from execution bottlenecks
If ./gradlew help --profile is slow, investigate
startup/settings/configuration/init scripts before compile/test
settings. If help is fast but
:gamma:test dominates, changing configuration cache or
eager build logic is unlikely to move the critical path enough.
Chapter 24’s Configuration Cache may remove compatible repeated configuration work, but it must not be used to hide unsupported configuration-time side effects. First make build logic lazy/deterministic; then measure cache reuse.
8. Memory/GC diagnosis without guesswork
Inspect org.gradle.jvmargs, test JVM settings, worker
isolation, and the CI memory limit together. Symptoms such as Daemon
disappearance/restart, long GC pauses, OOM, swapping, or
machine-wide slowdown require preserving Daemon logs and
host/container memory data.
Gradle’s Daemon has built-in performance monitoring for heap exhaustion/leak behavior. Disabling that monitor is not a normal optimization and is deliberately outside this lab.
9. Fresh-state comparison without deleting normal caches
export DIAG_HOME="$PWD/.diag-gradle-home"
rm -rf "$DIAG_HOME"
GRADLE_USER_HOME="$DIAG_HOME" ./gradlew --version
GRADLE_USER_HOME="$DIAG_HOME" ./gradlew perfPipeline --profile --console=plain | tee fresh-home.log
# Preserve evidence, then remove only the disposable diagnostic home.
rm -rf "$DIAG_HOME"
A fresh User Home changes Wrapper/dependency caches, Daemon registry/logs, and other user-level state simultaneously. Use it only when that controlled “fresh environment” question matters; do not mix it into a worker-count experiment.
10. Least-destructive repair order
- Restore correct task dependencies/inputs/outputs/tests/security gates.
- Normalize Wrapper/JDK/Daemon JVM identity between compared environments.
- Remove or defer unnecessary eager configuration work.
- Right-size workers/test forks/process isolation to the host.
- Enable project parallelism only if the graph exposes safe branches.
- Keep file watching where supported and beneficial; document exceptions.
- Reintroduce Chapter 24 caches only after task correctness remains proven.
- Repeat profile + test + artifact checks.
Knowledge check
A build gets slower after workers increase from 2 to 8. What is the first response?
Preserve both runs and host resource evidence, then test a smaller worker/fork budget. Do not assume a code defect or delete caches.
Why might changing org.gradle.jvmargs create
another Daemon?
Daemon reuse requires compatible immutable JVM arguments. Heap/JVM-arg changes make the existing Daemon incompatible with the new request.
Parallel execution exposes corrupted results.txt.
Is mustRunAfter the best repair?
Usually no. Give producers independent declared outputs and aggregate them through explicit data/task dependencies. Ordering alone can hide shared-state ownership defects.
What does disabling test prove about
performance?
Only that less work was performed. It does not prove an optimization of the required verification workload.
Why use an isolated diagnostic
GRADLE_USER_HOME?
It permits a controlled fresh-state comparison without deleting the normal user or shared CI state and destroying evidence.
help is slow but tests are fast. Which phase is
the stronger suspect?
Startup/settings/configuration/init/plugin work, because
help performs little application task execution.
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.