How does org.gradle.workers.max interact with --parallel, and how would you reason about setting it on a CI agent versus a developer laptop?
answer
- workers.max = global lease pool, default = cores
- shared with Worker API
- effective = min(ready tasks, free leases)
- container sees host cores -> over-parallelize/OOM
- pin explicitly on CI; measure with build scan
basics
~20 sorg.gradle.workers.max caps the number of concurrent worker leases the daemon hands out, defaulting to CPU core count. --parallel uses that pool to run cross-project tasks. On constrained or container CI agents you often lower it; for wide builds on big machines you may raise it.
solid answer
~50 s`--parallel` schedules ready tasks across projects onto **worker leases**, and `org.gradle.workers.max` sets the size of that lease pool (default = available processors). The same pool also backs the Worker API, so it's a single global concurrency budget — Gradle won't oversubscribe beyond it. Tuning is about matching that budget to real resources. On a **dev laptop**, the default is usually fine, but you might lower it to keep the machine responsive. On **CI**, the catch is containers: Gradle may see the host's core count rather than the cgroup CPU limit, so the default can wildly over-parallelize, thrash, and OOM. Set `org.gradle.workers.max` explicitly to the agent's real CPU allotment (and pair with sane `org.gradle.jvmargs` heap). Going too high causes context-switching and memory pressure; too low leaves the parallel frontier underutilized. Measure with build scans before committing a value.
code
toml · 4 lines# gradle.properties (4-vCPU CI container)
org.gradle.parallel=true
org.gradle.workers.max=4
org.gradle.jvmargs=-Xmx3ggo deeper
Know it caps concurrent tasks and defaults to core count.
Explain it sizes the shared lease pool and that effective parallelism is min(ready tasks, leases).
Reason about CI containers vs host cores, memory budget per fork, and measuring with build scans.
Standardize per-environment gradle.properties / init scripts across the org and bake CPU/heap budgets into the CI image.
## What the setting controls `org.gradle.workers.max` defines how many **worker leases** the daemon issues at once. A lease is required to run a task under `--parallel` *and* to run a unit of work submitted to the Worker API. Because both draw from the same pool, the value is a **single global cap on concurrency** for the build — this is deliberate, so CPU-bound work from different sources doesn't oversubscribe the machine. Default = number of processors Gradle detects. ## Interaction with --parallel `--parallel` provides *eligible* concurrency (independent cross-project tasks); `workers.max` provides the *capacity* to run them. Effective parallelism each moment = min(ready independent tasks, free leases). Raising `workers.max` only helps if the graph actually has a wide enough frontier; otherwise it does nothing. ## Developer laptop - Default is usually right. - Lower it (e.g. to cores − 1 or − 2) if you want the IDE/browser to stay responsive during big builds. - Remember memory: each concurrent compile/test forks JVMs; more leases means more peak heap and metaspace. ## CI agents — the container trap The classic failure: a build runs in a container limited to, say, 2 vCPUs via cgroups, but the JVM/Gradle reports the *host's* 32 cores. Gradle then issues up to 32 leases, forks dozens of test JVMs, thrashes the CPU quota, and OOM-kills. Mitigations: - Set `org.gradle.workers.max` explicitly to the container's CPU allotment. - Set `org.gradle.jvmargs=-Xmx…` so the daemon heap fits the container memory; size test `maxParallelForks` deliberately too. - On modern JDKs, container-awareness helps detect cgroup limits, but pinning the value removes ambiguity. ```properties # gradle.properties on a 4-vCPU CI container org.gradle.parallel=true org.gradle.workers.max=4 org.gradle.jvmargs=-Xmx3g -XX:MaxMetaspaceSize=512m ``` ## How to choose a value 1. Start from real allocated CPUs (not host cores). 2. Account for memory: total peak heap ≈ leases × per-fork heap must fit RAM. 3. Use **build scans** / `--profile` to see actual concurrency and the critical path; if the frontier is narrow, more leases won't help. 4. Iterate: raise until throughput plateaus or memory/CPU saturates, then back off. ## Common mistakes - Cranking it to a huge number 'to go faster' — causes context-switch overhead and GC pressure. - Forgetting test forking (`Test.maxParallelForks`) compounds with `workers.max`. - Tuning leases while the real bottleneck is a long critical path or disk I/O.
- Why can the default workers.max be dangerous inside a CI container?Gradle may detect the host's core count rather than the cgroup CPU limit, issuing far more leases than the quota allows — leading to thrashing and OOM kills.
- Does raising workers.max always speed up a build?No. It only helps if the task graph has enough independent ready tasks; a narrow/critical-path-bound graph won't use the extra leases.
- What other setting compounds with workers.max for test parallelism?Test.maxParallelForks forks multiple test JVMs; combined with high workers.max it can multiply memory and CPU usage unexpectedly.
saying these in an interview costs you the question
- Setting workers.max arbitrarily high to 'maximize speed'.
- Ignoring container CPU/memory limits and trusting the detected core count.
- Tuning leases without profiling — when the bottleneck is the critical path or I/O.