Even with the daemon disabled, how can Gradle leave processes behind on a CI agent, and how do you ensure a clean exit?
answer
- build forks many JVMs
- Worker API process isolation
- Kotlin compile daemon ≠ Gradle daemon
- forkEvery test JVMs
- container teardown reaps all
basics
~20 sA build forks more than the main JVM — worker daemons for compilation/tests. With the build daemon disabled you still want these to exit; use --no-daemon plus killing stray Java processes or running in a destroyed-per-job container.
solid answer
~40 sDisabling the **build daemon** with `--no-daemon` covers the main build JVM, but a Gradle build also forks other JVMs: the **Worker API** processes (e.g. forked compilers, the **Kotlin compile daemon**, forked test JVMs via `Test.forkEvery`, annotation processors). These have their own lifecycles and can outlive a poorly-terminated build. For a guaranteed clean CI exit: 1. Prefer **destroyed-per-job ephemeral agents** — when the container is torn down, every child process dies with it. This is the most robust answer. 2. On **reused runners**, add explicit cleanup: `./gradlew --stop` to kill build daemons, and a teardown step that kills lingering `*GradleWorkerMain*`/Kotlin-daemon processes. 3. Cap memory on forked workers so a leaked one is less damaging. The key insight: `--no-daemon` is necessary but not sufficient; container teardown is what truly guarantees no orphans.
code
bash · 5 lines# reused-runner teardown that actually removes orphans
./gradlew build --no-daemon
./gradlew --stop
pkill -f 'GradleWorkerMain' || true
pkill -f 'KotlinCompileDaemon' || truego deeper
Recognise that a build can spawn more than one process; details optional.
Name worker/test/Kotlin forks and know that --no-daemon only covers the build daemon.
Design a clean-exit strategy: ephemeral teardown first, explicit --stop + kill for reused runners.
Standardise leak-free runner images and teardown across the org; weigh self-hosted reuse against the cleanup burden.
## More than one JVM A Gradle build is not a single process. The launcher forks a build JVM (the daemon, or a single-use daemon under `--no-daemon`), and that build can fork **additional** JVMs: - **Worker API processes** — Gradle's `WorkerExecutor` can run work in `ProcessIsolation`, forking a `GradleWorkerMain` JVM. - **Forked compilers** — Java compilation with `options.fork = true`, and the **Kotlin compile daemon** (a separate long-lived JVM the Kotlin plugin spins up to keep the compiler warm). - **Forked test JVMs** — `test { forkEvery = N; maxParallelForks = M }` launches test executor JVMs. - **Annotation processors / tools** run in some of the above. So even with `org.gradle.daemon=false`, these worker/compiler/test JVMs exist during the build and, if the build is killed abruptly or the tool keeps a warm process, can linger. ## Why orphans happen - A timed-out CI step sends a signal to the launcher but child JVMs may not get a clean shutdown. - The **Kotlin compile daemon** is explicitly designed to stay warm; it is a separate process from the Gradle daemon and is not stopped by `--no-daemon`. - On a **reused** runner, anything left behind survives into the next job. ## Ensuring a clean exit ```bash # 1) the robust default: ephemeral, destroyed-per-job container # container teardown reaps ALL child processes — no orphans possible # 2) on reused/self-hosted runners, be explicit: ./gradlew build --no-daemon ./gradlew --stop # stop any build daemons # teardown step: kill lingering workers/kotlin-daemon pkill -f 'GradleWorkerMain' || true pkill -f 'KotlinCompileDaemon' || true ``` ```properties # constrain forked workers so a leaked one is bounded org.gradle.daemon=false ``` ## Decision guidance - **Best practice:** make agents **fully ephemeral**; the OS/container lifecycle is your cleanup. `--no-daemon` then mainly buys predictability, not leak-prevention. - **Self-hosted reused pools:** combine `--no-daemon`, `--stop`, an explicit kill/teardown, and bounded worker memory. - Disabling Gradle's daemon does **not** disable the Kotlin compile daemon or worker forks — know that distinction when debugging "why is there still a Java process?" ## Summary `--no-daemon` addresses the *build* daemon. True orphan-freedom on CI comes from agent teardown plus, where agents are reused, explicit termination of the build daemon, worker processes, and tool daemons.
- Does --no-daemon stop the Kotlin compile daemon?No. The Kotlin compile daemon is a separate warm process spawned by the Kotlin plugin; it is unrelated to org.gradle.daemon and survives --no-daemon. You stop it explicitly or by destroying the agent.
- What is the single most reliable way to guarantee no orphaned processes on CI?Run each job on a fully ephemeral agent that is destroyed afterward — container/VM teardown reaps every child JVM, so no daemon or worker can leak into the next job.
- Which Gradle setting causes extra forked test JVMs?Configuring the Test task with forkEvery and maxParallelForks launches separate test executor JVMs that run alongside the build process.
Switching off the main engine (--no-daemon) doesn't shut the auxiliary generators (worker/Kotlin/test JVMs); scrapping the whole vehicle (destroying the container) is what guarantees nothing is left running.
saying these in an interview costs you the question
- Assuming --no-daemon kills every JVM the build spawns, including the Kotlin compile daemon and worker processes.
- Relying on pkill on a shared runner without scoping, risking killing unrelated builds.
- Treating the build daemon and worker/compiler daemons as the same process.