skip to content

A team reports that even `gradle help` is slow in their large monorepo. How would you diagnose and reduce configuration-phase time?

level: seniorimportance: should knowfreq 40%

answer

  1. help = pure config/init signal
  2. --profile / build scan to localize
  3. eager create, script-body exec/IO, .get()
  4. fix convention plugins first (multiplier)
  5. configuration cache skips the phase

basics

~10 s

Since help runs no real tasks, slowness is configuration-phase cost. Profile with --profile/build scans, find eager create, script-body I/O and .get() calls, switch to lazy register/Provider APIs, and enable the configuration cache.

solid answer

~50 s

`gradle help` executes essentially no task work, so its time is dominated by **initialization + configuration**. In a large monorepo, configuration cost scales with module count because all projects are configured by default. Diagnosis: run `gradle help --profile` or a build scan to see per-project configuration time, and look for the usual offenders — eager `tasks.create`, file-system scans, `project.exec`/network calls, and `.get()` on providers in script bodies, often hidden in shared convention plugins so they multiply across modules. Remediation, in order of impact: (1) make tasks lazy with `tasks.register` and use `configureEach`/`withType(...).configureEach`; (2) move work out of script bodies into task actions or lazy `Provider`/`Property`; (3) avoid eager container iteration that realizes tasks; (4) enable the **configuration cache** so unchanged builds skip configuration entirely. The cache is the biggest structural win but requires making the build configuration-cache compatible.

code

bash · 3 lines
bash
gradle help --profile            # localize per-project config cost
gradle help --scan               # hot scripts & plugins
gradle help --configuration-cache  # skip config on unchanged inputs

go deeper

for a junior

Recognize that help is slow because of configuration, not execution.

for a middle

Name the offenders (eager create, script-body I/O) and the lazy-API fixes.

for a senior

Drive a measured workflow: profile, fix convention plugins first, adopt the configuration cache, verify and guard in CI.

for a principal

Own configuration-time as an org SLO: enforce cache compatibility and lazy conventions, add CI gates, and budget configuration cost against module growth.

## Why `gradle help` is a clean signal `help` runs a trivial built-in task and no project work, so its wall-clock time isolates **initialization + configuration** cost from execution. If `help` is slow, the configuration phase is the suspect — and because Gradle configures all projects by default, configuration time grows with the number of modules. ## Step 1 — measure ```bash gradle help --profile # HTML report: configuration vs execution per project gradle help --scan # build scan: performance breakdown, hot scripts gradle help --configuration-cache # see if a cache can be reused ``` The profile/scan shows which projects and which plugins dominate configuration. Don't optimize by guess — find the hot modules first. ## Step 2 — find the offenders Common configuration-time taxes, especially when they live in a **shared convention/precompiled-script plugin** applied to every module (so the cost multiplies): - **Eager task creation:** `tasks.create(...)` realizes and configures tasks unconditionally. - **Script-body I/O:** `file(...).walk()`, reading property files, `project.exec`/`providers.exec(...).get()` to shell out (e.g. `git rev-parse`). - **Premature dependency resolution:** resolving a `Configuration` at configuration time (`configurations["x"].files`). - **Eager container iteration:** `tasks.forEach { }`, `withType(...).all { }`, or `.get()` on a `TaskProvider`, which realize tasks you registered lazily. ## Step 3 — remediate (highest leverage first) 1. **Lazy task APIs.** Replace `create` with `register`; replace `all { }`/`forEach` with `configureEach`/`withType(...).configureEach { }`. 2. **Defer real work.** Wrap computed values in `providers.provider { }` / `Property`, and only resolve (`.get()`) inside task actions. Move file/exec/network work into `@TaskAction`. 3. **Fix convention plugins first.** Because they apply to every module, one eager line there is the highest-multiplier offender. 4. **Enable the configuration cache.** Once the build is cache-compatible (no `Project` access at execution, no disallowed inputs), Gradle serializes the configured graph and **skips the configuration phase** on unchanged builds — turning an O(modules) cost into near-zero on cache hits. ```kotlin // Before (in a convention plugin, runs for every module, every build): val branch = providers.exec { commandLine("git", "rev-parse", "--abbrev-ref", "HEAD") } .standardOutput.asText.get() // eager .get() at config time version = "1.0-$branch" // After: keep it lazy; resolve only where needed at execution val branchProvider = providers.exec { commandLine("git", "rev-parse", "--abbrev-ref", "HEAD") } .standardOutput.asText // no .get() tasks.register("printVersion") { doLast { println("1.0-" + branchProvider.get()) } } ``` ## Step 4 — verify and guard Re-profile after each change; confirm `help` time drops. Add the configuration cache to CI so regressions (newly-introduced eager work that breaks the cache) fail fast. Track configuration time as a build-health metric. ## Summary `help` slowness = configuration cost. Measure with profile/scan, eliminate eager realization and script-body work (especially in shared plugins), and adopt the configuration cache to make the phase skippable. The result is configuration time that stays bounded as the monorepo grows.

  • Why fix convention/precompiled-script plugins before individual modules?
    A convention plugin applies to every module, so a single eager line there is paid N times per build. Fixing it has the highest multiplier on configuration time.
  • What prerequisite does enabling the configuration cache impose?
    The build must be configuration-cache compatible: no Project access at execution time, no disallowed runtime inputs, and tasks must declare their inputs properly. Otherwise Gradle reports cache-incompatibility problems.
  • How does the configuration cache differ from the daemon for this problem?
    The daemon keeps a warm JVM but still re-runs configuration each build. The configuration cache reuses the serialized configured graph, actually skipping the configuration phase on unchanged inputs.

saying these in an interview costs you the question

  • Blaming execution/test time for slow `help`, which runs no real tasks.
  • Recommending the build cache (output reuse) as the fix for configuration-phase cost.
  • Optimizing random modules without profiling to find the hot ones.

context