skip to content

On CI, builds frequently start with a cold daemon. What performance cost does that impose, and how do you reason about whether to keep daemons warm there?

level: seniorimportance: should knowfreq 38%

answer

  1. CI = fresh container = cold daemon
  2. re-pays boot + JIT each job
  3. warmth inheritable only if process survives
  4. ephemeral → lean on remote cache
  5. don't reflexively --no-daemon

basics

~10 s

A cold daemon re-pays JVM startup and JIT warmup every job, so CI builds lose the amortization a developer enjoys. Keeping daemons warm helps long-running runners but conflicts with ephemeral, clean-container CI.

solid answer

~50 s

On CI, each job often runs in a fresh container, so the daemon is **cold**: it re-pays JVM boot, class loading, and HotSpot JIT warmup before it's running at full speed. For short builds this overhead is a large fraction of total time; for long builds it's amortized away within the single run. The decision hinges on **build duration and runner topology**. On **persistent, reused runners**, keeping a daemon alive across jobs lets later jobs hit a warm JVM — real savings. On **ephemeral runners** (a new container per job), there's no process to reuse, so warmth can't be inherited; the lever instead is the **build cache** (avoids re-running task work) and not re-paying warmup needlessly within a job — e.g., avoid `--no-daemon` so configuration/resolution at least warm up once within the run. Historically CI used `--no-daemon` to avoid leaked daemons; modern guidance is to allow the daemon within a job and rely on container teardown for isolation.

code

bash · 4 lines
bash
# Let the daemon warm within the job and reuse a remote cache:
./gradlew build --build-cache
# vs. forcing a cold, un-reusable JVM every time:
./gradlew build --no-daemon

go deeper

for a junior

Know CI often starts cold and re-pays startup, so CI builds can feel slower per run.

for a middle

Distinguish JVM warmth (needs a surviving process) from the build cache (skips task work) and that ephemeral runners can't inherit warmth.

for a senior

Reason about runner topology to choose daemon-reuse vs. cache-only, and justify not blindly using --no-daemon.

for a principal

Set fleet-wide CI policy: persistent vs. ephemeral runners, remote cache, daemon hygiene, balancing cost, isolation, and build-time SLAs.

## Cold vs. warm on CI A developer's machine keeps **one daemon alive all day**, so most builds are warm. CI is different: many setups spin up a **clean container per job**, giving every build a **cold daemon** that must: 1. boot the JVM, 2. load + initialize Gradle/plugin classes, 3. run interpreted until HotSpot's JIT compiles the hot paths. For a 10-second build, this warmup can be a big share of wall time; for a 10-minute build it's noise. ## The two levers There are two distinct optimizations, and conflating them is a common mistake: - **JVM warmth** (this topic): cheap *build-logic* CPU via a warm JIT/loaded classes. Inheritable **only** if a real daemon **process survives** between jobs. - **Build cache**: skips *task execution* by reusing outputs. Works across machines/jobs via a **remote cache**, independent of any warm process. ## Topology decides the warmth play | Runner type | Can inherit warmth? | What to lean on | |---|---|---| | Long-lived / self-hosted, reused | Yes — keep the daemon alive across jobs | Daemon reuse + remote cache | | Ephemeral container per job | No — process dies with the container | Remote build cache; allow daemon **within** the job | ## Practical guidance - **Don't reflexively pass `--no-daemon`.** The old fear was leaked daemons consuming RAM on shared machines; on an ephemeral runner the container teardown already disposes the process, and allowing the daemon lets the single job warm up once (configuration phase, resolution) and reuse that within the run. - For **self-hosted persistent runners**, deliberately keeping a daemon alive across jobs converts later jobs to warm — a genuine speedup, at the cost of needing memory hygiene and occasional restarts. - Pair either choice with a **remote build cache** so task work isn't repeated, which usually dwarfs warmup savings anyway. ```bash # CI: prefer allowing the daemon within the job (default), plus a remote cache, # rather than forcing a fully cold, no-daemon run every time. ./gradlew build --build-cache # daemon allowed; remote cache configured in settings # Avoid (unless you have a specific reason): ./gradlew build --no-daemon # re-pays full warmup, no in-run reuse benefit ```

  • If runners are ephemeral, how do you still get cross-job speedups?
    You can't inherit JVM warmth across dead containers, so you rely on a **remote build cache** to skip task execution across jobs/machines, and you allow the daemon within each job so at least that run warms up once.
  • What's the risk of keeping a daemon alive across jobs on a shared self-hosted runner?
    Memory accumulation and state leakage between builds; daemons can grow their heap or hold stale state, so you need memory limits, periodic restarts, and idle-timeout policies.
  • Does warmup matter for a 15-minute build?
    Barely — warmup is a one-time cost amortized over the run, so for long builds it's a tiny fraction; the cache and parallelism dominate there.

saying these in an interview costs you the question

  • Claiming a remote build cache makes a cold JVM warm — the cache skips task work, it doesn't pre-compile Gradle's JIT.
  • Always forcing `--no-daemon` on CI 'for safety' without considering in-run warmup loss.
  • Assuming warmth can be carried across containers that are destroyed after each job.

context