skip to content

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?

level: seniorimportance: should knowfreq 35%

answer

  1. workers.max = global lease pool, default = cores
  2. shared with Worker API
  3. effective = min(ready tasks, free leases)
  4. container sees host cores -> over-parallelize/OOM
  5. pin explicitly on CI; measure with build scan

basics

~20 s

org.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
toml
# gradle.properties (4-vCPU CI container)
org.gradle.parallel=true
org.gradle.workers.max=4
org.gradle.jvmargs=-Xmx3g

go deeper

for a junior

Know it caps concurrent tasks and defaults to core count.

for a middle

Explain it sizes the shared lease pool and that effective parallelism is min(ready tasks, leases).

for a senior

Reason about CI containers vs host cores, memory budget per fork, and measuring with build scans.

for a principal

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.

context