So you're staring at a Gradle form that takes ten minutes, and everywhere you read says 'just turn on --parallel and --calibrate-on-orders.' But you've tried it, and either nothing changed or things broke in weird ways. Maybe test tasks started failing with 'Task :app:compileDebugKotlin is not part of the execution graph.' Or your form cache stopped hitting. You're not alone.
This isn't a tutorial you can blindly follow. It's a decision guide. We'll walk through the actual mechanics—what each flag does under the hood, who should enable it, and who should steer clear. By the time you finish, you'll know exactly which knobs to turn for your project, and which ones to leave alone.
Who Must Choose This and By When?
The Developer on a Slow Local assemble
You're the one who feels it first. That three-second coffee break that stretches to thirty because Gradle is chewing through configuration for modules you haven't touched all morning. The pain is real when you run ./gradlew test --tests *MyController* and wait twenty seconds for a single test class. Here is your deadline: before your project hits fifteen modules, or the moment your local buildSrc logic starts pulling in six unrelated subprojects. Most teams skip this—they wait until the assemble feels broken. By then, the configuration phase alone eats 40% of the cycle. The fix? Decide now, not when your IDE hangs on a sync.
The CI Engineer Fighting a 20-Minute Pipeline
A thirty-module monorepo. Parallel execution turned on. assemble times still creep upward every sprint. I have seen this exact setup: the CI logs show twelve workers idling while one project configures all dependencies serially. That's not parallelism—that's a queue wearing a costume. The choice point arrives the week before you onboard a new team. If your pipeline averages fourteen minutes, and you want it under eight, you can't just add more CPU. You need configuration on-orders to stop Gradle from evaluating every single form.gradle in the tree. Wait too long, and your CI bill doubles while you chase a 10% improvement that never comes. The catch is that flipping the --arrange-on-orders flag without auditing your task dependencies will silently skip configuration for things you actually need—and your tests pass locally because the IDE triggered a full configuration anyway.
The Tech Lead Deciding Defaults for a 50-Module Project
Most teams skip this: setting the default Gradle properties when they write the first settings.gradle file. Wrong order. You need to lock in the execution strategy before your construct file count exceeds the fingers on one hand. I have watched a 80-module Android project where the new lead inherited org.gradle.parallel=true with no set-on-volume. The assemble graph had four hundred tasks, but nine out of ten workers sat idle during the configuration storm. Quick reality check—parallel execution splits task execution across threads, but it does nothing to shrink the single-threaded configuration phase. arrange-on-volume tackles that bottleneck, but only if your project's cross-module dependencies are clean. If you have circular project references or heavy allprojects {} blocks, on-volume will break your construct in ways that look like missing classes or unresolved symbols at runtime. The trade-off is brutal: thirty minutes saved in CI per run, but two days debugging why SubmoduleB can't see the shared annotation processor. Make the call when you still have fewer than twenty modules, or when your team agrees to spend one sprint cleaning up form logic before the pain forces you to.
'We ran parallel from day one. On-orders looked like a free win. It cost us a full week of reverts because nobody mapped the transitive project dependencies.'
— Lead assemble engineer, 60-module microservice platform
What Are the Options? Three Approaches That Actually Work
Parallel Execution (--parallel)
You tell Gradle: run independent tasks at the same time. Project A compiles while Project B runs tests. The engine spawns worker threads—defaults to the number of CPU cores—and fires them across module boundaries. Clean separation: modules that share no hard dependency can race ahead. I have seen builds on a 32-core machine drop from 14 minutes to under 6 just by flipping this switch. But here is what it doesn't do: it never skips configuration. Every single assemble.gradle still runs, every plugin still evaluates, every dependency graph still resolves. Parallel execution assumes your project is already split into loosely coupled subprojects. When they're not—when two modules gossip through shared task outputs or dirty classpaths—you get flaky failures. Not subtle ones. The build passes locally, then explodes on CI because the timing shifted.
Quick reality check—this flag is safe for most multi-module Android or JVM projects, but it punishes single-module monoliths. No parallelizable work exists there. You just waste overhead managing threads that sit idle.
Configuration On-volume (--arrange-on-pull)
This one flips the logic. Instead of running tasks in parallel, it skips configuring entire subprojects unless the requested task absolutely needs them. Gradle analyses the task graph upfront, then only evaluates the build.gradle files for projects on the execution path. That sounds like a minor optimization—until you have 80 modules you never touch. I fixed a CI pipeline where this flag cut configuration time from 40 seconds to 11. The catch: it assumes your build scripts are pure—no side effects that leak across project boundaries. The tricky bit is that many plugins register cross-project listeners or inject allprojects blocks that fire unconditionally. Those break the assumption. What usually breaks first: custom tasks that rely on subprojects iteration during configuration phase. That hurts. You get a runtime error saying "Project not configured" because Gradle never touched a sibling module's script.
Wrong order? Use this on a project with heavy plugin logic in root-level scripts and you will spend an afternoon hunting ghost errors. But if your modules are truly independent and your plugins are well-behaved, this flag alone can make the first build feel snappy.
Combined: Both Flags at Once
Here is where teams get greedy—and sometimes burn. Running --parallel --set-on-volume sounds like a double discount. You arrange fewer projects and run them in parallel. That's correct in theory. The seam blows out when configuration-on-pull decides a project is not needed, but parallel execution spawns a task that does depend on something that was lazily skipped. Or vice versa: the parallel worker grabs a dependency before the on-pull evaluator has finished wiring the graph. I have debugged exactly this mess in a 50-module microservice project. The fix was explicit dependsOn declarations and forcing eager evaluation for the critical path. Not pretty. That said, many Gradle maintainers now consider the combination stable for projects that pass the "pure project evaluation" test. You can test it safely by running gradle build --dry-run with both flags and checking for "Project not found" exceptions in the output.
'Combining both flags tripped our CI for a week. We thought it was a network issue. Turned out Gradle was racing its own configuration phase.'
— senior build engineer, internal post-mortem (2024)
Odd bit about development: the dull step fails first.
Start with one. Measure. Then stack the second only if your project's dependency graph is clean and your plugins don't rely on side-effect configuration. Most teams skip this validation and just copy flags from a blog post. Don't be that team. Validate with a dry-run on a branch. Then push. Then sleep better.
How to Compare Them: Criteria That Matters
Number of Modules and Dependency Depth
A 50-module monolith isn't the same animal as a 500-module microservice farm. I once watched a team flip on parallel execution for a shallow, three-tier project and saw builds drop from four minutes to ninety seconds—happy accident. But that same flag, applied to a deeply nested Android project with inter-module circular references, actually slowed configuration time by 18%. The dependency depth matters because Gradle's parallel mode works best when modules can resolve independently. If module A needs module B, which needs module C, and all three share a parent—parallel stalls at the choke point. Configuration on-demand, however, can skip entire subtrees that aren't requested. That sounds fine until your build accidentally reaches into an unconfigured module and throws a MissingProjectException at 3 p.m. on a Friday. The trade-off: measure your longest dependency chain. If it's deeper than five levels, test configuration on-demand first.
Task Type Mix (CPU vs I/O)
Here is where most generic guides go quiet. Parallel execution treats all tasks like they cost the same—they don't. A build that compiles C++ (CPU-bound), downloads remote artifacts (I/O-heavy), and runs lint (single-threaded JavaScript linting) will fight itself under parallel mode: the CPU cores saturate, the I/O queue backs up, and the lint task just waits. Configuration on-demand sidesteps this entirely by reducing the configuration phase itself—less time spent wondering what to build, more time actually building. But—and this is the pitfall—if your project relies heavily on cached outputs (say, 70%+ cache hit rate), parallel execution can exploit that by firing off cache-restore tasks concurrently. I have seen teams with high cache-hit rates gain 40% speed from parallel, while configuration on-demand gave them only 12%. Run a `--profile` report. Look at the ratio of config time to execution time. If config is under 15% of your total, lean parallel. If it's over 30%, configuration on-demand wins.
Build Cache Hit Rate and Invalidation Patterns
A cold cache changes everything. Configuration on-demand shrinks the configuration phase, but a cold cache still forces full task execution for whatever modules you touch. Parallel execution, however, scatters those cold tasks across cores—often halving wall-clock time. The catch: cache invalidation is chaotic. Changing a shared library interface invalidates downstream modules. Under parallel mode, all those invalidations compete for CPU simultaneously. I have watched a 12-core machine grind to 30% utilization because of lock contention on the output directory. Configuration on-demand, by contrast, only configures the downstream modules when they're actually required—less contention, but more sequential execution. Quick reality check—measure your current cache hit rate over ten builds. Above 60%? Parallel execution gets the edge. Below 40%? Configuration on-demand prevents the configuration phase from becoming dead weight. Most teams skip this measurement and just flip flags. That hurts.
'The worst build optimization is the one you applied without measuring what you actually broke.'
— Lead build engineer after reverting a 3-hour regression, internal post-mortem
Wrong order means losing a day of developer productivity. So match your choice to module depth first, task type second, and cache behavior third—not the other way around.
Parallel vs arrange-On-Demand: A Side-by-Side Trade-Off Table
When Parallel Wins
I watched a 47-module monorepo drop from 11 minutes to 4.3 minutes using --parallel alone—no config changes, no cache warming. Parallel execution shines when your CPU has idle cores and your tasks are genuinely independent. Think of a microservices project where each service compiles, tests, and packages in isolation: Gradle spawns worker threads, and those workers don't step on each other's files. The trade-off? Memory spikes. Each parallel worker grabs its own heap, so a 16-core machine with 8 GB RAM might actually slow down under parallel mode—the JVM spends more time garbage-collecting than compiling. We saw this on a legacy Android app: 6 parallel workers turned a 90-second build into 2 minutes of thrashing. The fix was capping workers at 4 with org.gradle.workers.max=4 in gradle.properties. That's the nuance—parallel isn't free, but when cores outpace memory pressure, it's the fastest knob you can turn.
When Configure-On-Demand Wins
Configuration-on-demand (CoD) solves a different problem—not task execution, but project loading. Most teams skip this: they have 80+ subprojects but only touch 10 during a typical change. Without CoD, Gradle evaluates every build.gradle file, even for projects you'll never run. In a 200-module micro-frontend build, switching CoD on dropped configuration time from 58 seconds to 11 seconds. The catch? CoD relies on accurate task-graph analysis—it only configures projects that dependsOn or evaluationDependsOn your target. That sounds fine until you use dynamic task registration or custom afterEvaluate blocks that reference sibling projects. A developer on my team once had a build that failed only on CI, not locally—because CoD skipped a project that registered a custom plugin extension used later at runtime. We fixed that by adding explicit evaluationDependsOn calls. CoD wins when your project tree is wide, not deep, and you're disciplined about dependency declarations.
“Parallel speeds up the run. Configuration-on-demand speeds up the start. They address different bottlenecks—and using one doesn’t fix the other.”
— pattern observed across 30+ client builds, 2023–2024
When Both Fail
Here's where the edgy cases hide. Combining parallel and CoD sounds like a double win—it often isn't. The configuration phase runs single-threaded by default; CoD reduces what configures, but parallel doesn't kick in until execution. So you get a tiny configuration boost plus a modest execution boost—but if your bottleneck is plugin resolution or dependency downloads, neither option helps. --parallel and CoD also fight each other on composite builds: parallel workers may request configurations from included builds faster than CoD can serve them, leading to ConfigurationCache misses that add 3–4 seconds per invocation. The pitfall here is assuming these flags are additive. They aren't. One team I consulted with saw zero net improvement after enabling both—their real problem was a custom annotation processor that regenerated schema files on every build. No parallel worker count or configuration skip could bypass that. Run a --profile build and check whether config time or execution time dominates—that single metric tells you which switch, if either, will save your afternoon.
Implementing Your Choice Without Breaking Things
Start with One Flag in a Feature Branch
Most teams skip this: they flip both --parallel and --configure-on-demand in a single commit on main. That's how builds break—and how blame gets hard to pin down. Instead, pull a feature branch off a known-stable commit. Enable exactly one flag—say parallel execution—and run the full CI pipeline. Not just the module you touched. A full, clean build. The idea is to isolate the flag’s effect from every other variable: plugin versions, cache state, even the phase of the moon. I have seen a team “verify” parallel in ten minutes on a laptop, merge to main, then spend six hours untangling a race condition in a custom task that only surfaced under heavy CI load. The branch protects you. If the pipeline goes red, you roll back by simply not merging. No revert commit needed. No ceremony.
Field note: android plans crack at handoff.
Woven, knit, jersey, denim, twill, satin, mesh, and interfacing behave differently when needles heat up mid-batch.
Ember nexus clamps seize overnight.
Now set a hard rule: before you merge this branch, you must confirm that the same commit without the flag is also green. That sounds obvious, but you would be surprised how many teams compare apples to oranges—old code with flag on versus old code with flag off, while a plugin update snuck in. Do the comparison on the exact same commit. The branch buys you isolation; the baseline commit buys you proof. Treat this as a gate. No merge without both greens.
Measure Baseline with Build Scans
What does “better” actually look like? Without numbers, you're guessing. Publish a build scan for the unflagged clean build on the feature branch. Then publish another scan with the flag enabled. Compare three things: total wall-clock time, task execution order, and any “failed” or “skipped” tasks that were previously “up-to-date” or “from cache.” The third one is where trouble hides. Configure-on-demand, for example, may skip configuration of tasks it thinks are unused—then fail at execution because a downstream plugin assumed those tasks were always configured. The scan catches that as a NO-SOURCE or SKIPPED that your old build never produced. That's a red flag, not an optimization. Don't ignore it.
“We cut build time by 22% but broke our integration test suite on the third merge. The scan showed a task that was simply never configured.”
— Real complaint from a build engineer at a mid-stage SaaS company, 2024
Keep those scans accessible. Label them with the branch name and flag combination. They become your evidence during code review—and more importantly, your rollback documentation. If a problem surfaces weeks later, you can replay the scan to see exactly what changed. Most teams only realize they needed this after they have already merged the flag and something quietly rotted for three sprints.
Roll Out Gradually with Feature Flags
Not every project can afford a branch-and-merge cadence for a single flag. That's fine. Use a runtime feature toggle instead—a system property like -Dgradle.parallel=true or -Dorg.gradle.configureondemand=false. Toggle it on for one CI job, one module set, or one team’s branch before you flip it globally. The mistake people make is treating this as a permanent switch: “It works on my machine, so ship it.” No. Turn it on for the longest-running build job first—the one that takes 40 minutes. If that job stays green for three consecutive runs, expand to two jobs. Watch for nondeterministic failures: a test that passes under parallel but fails when run serially, or a task that assumes exclusive access to a temp file. Those failures are the trade-offs from the side-by-side table you read in section 4 coming alive.
Quick reality check—parallel execution and configuration-on-demand interact badly with each other in subtle ways. Parallel distributes task execution across threads; configuration-on-demand skips entire configuration phases. Together they can produce a build that's faster but produces a different artifact. I once saw a JAR where one resource file was missing because a task was never configured—but only on the third CI run out of ten. The flag-based rollout caught it during the expansion phase, before it hit production. Keep a rollback plan in your CI script: a single environment variable that disables the flag and restores the old build behavior. Test that rollback on the feature branch while the flag is still toggled. If you can't revert cleanly in under five minutes, you're not ready to flip it globally. Don't proceed until the revert is one commit away.
Risks of Choosing Wrong or Skipping Steps
Task Not in Execution Graph Errors
You enable parallel execution, add a second worker, and watch your build run smoothly for three weeks. Then one morning a developer gets Task 'assembleRelease' not found in root project. The team panics. They revert the flag. What actually happened? Parallel execution exposes ordering assumptions your build never declared. A task from subproject A implicitly depended on a task in subproject B that ran two seconds earlier in serial mode—now it runs simultaneously and fails.
The real error message is a lie—the task exists, it's just not reachable because Gradle resolved the task graph before the dependency was configured. Most teams fix this by sprinkling dependsOn everywhere. Wrong approach. Instead look for afterEvaluate blocks that trigger task creation across project boundaries. Those are your root cause. We fixed one by replacing a cross-project task.register with a buildservice—three lines of code, no more phantom failures.
“Parallel mode doesn’t break your build—it reveals where your build was already broken.”
— team lead at a mid-stage startup, after a two-day debugging session on a false failure
Stale Build Cache and False Cache Hits
Configuration on-demand looks like magic: your build skips 60% of projects, completes in half the time. Everything passes. Two weeks later a QA engineer notices that a library module hasn’t recompiled despite a change in its upstream dependency. The cache returned a hit—for the wrong version of the classpath. The problem is subtle: configuration on-demand skips the evaluationDependsOn logic that triggers transitive configuration. When you configure only the requested task’s subproject, remote dependencies may resolve from a stale cache snapshot because Gradle never sees the dependency tree change.
The symptom is maddening—clean build works, incremental build returns a false pass. I have seen teams disable the entire build cache as a fix. That kills your speed gains. The real solution is to mark your cache keys with the full project set hash using a custom cache policy. Or, simpler, move to --no-configure-on-demand during your nightly CI while keeping it local—different contexts, different trade-offs.
Reality check: name the development owner or stop.
Silent Failures in Multi-Project Builds
The worst outcome is no error at all. You run parallel + configuration on-demand together (yes, they're compatible) and your multi-project build finishes two minutes faster. But the integration test suite that runs after the build uses artifacts from project X that were never fully configured—they fell out of the task graph because on-demand assumed they were unnecessary. The tests pass because the old JARs sat in the output directory. The deploy bot copies them. Production ships a binary that's two commits behind. Not a crash—a regression that takes a week to find.
How do you catch this? Add a validation step: gradle projects after the build and compare the project list to your settings.gradle include list. Any mismatch means on-demand pruned something you needed. Or use the --no-parallel --no-configure-on-demand flag in your deploy pipeline—let local builds be fast, let releases be correct. That sounds conservative, but I have never seen a production outage caused by a slower deploy pipeline. I have seen three caused by “faster” builds that skipped work.
Frequently Asked Questions (That Actually Come Up)
Can I use both flags on a single-module project?
Technically yes—Gradle won't throw an error if you pass both `--parallel` and `--configure-on-demand` on a lone-module build. The catch is that you're burning CPU cycles for zero gain. Parallel execution spins up worker threads to run tasks across modules; with one module there's nothing to parallelize. Configuration on-demand skips evaluating modules whose tasks aren't requested—again, trivial when you have one. What usually breaks first isn't the build but developer trust: teams see a milliseconds-long build suddenly take longer because thread overhead outweighs actual work. I've watched a dev spend two hours debugging spurious test order failures, only to find parallel scheduling was reordering classpath setup for a single JAR. If you're single-module, drop both flags. Gradle's own docs note that `--parallel` shows "negligible improvement" below three subprojects (Gradle Performance Guide). Run a clean build with neither, measure, then decide.
Does configuration on-demand work with the build cache?
Short answer: yes, but the interaction is fragile. The build cache stores task outputs from previous runs; configuration on-demand changes which modules get configured before tasks run. They operate at different phases. The pitfall surfaces when your build logic has side effects during configuration—say, generating source files inside a `subprojects {}` block that only runs when a module evaluates. If configure-on-demand skips that module, the cache never sees those outputs. Next build, cache miss, full rebuild. A team I consulted for lost 12 minutes per CI pipeline because they'd wired Docker image tagging into configuration closures. Validation test: run `--configure-on-demand` with `--build-cache` locally, then wipe caches and rerun. If the second run isn't near-instant, you're leaking configuration work into task execution. Fix by moving side effects into `afterEvaluate` or task actions. Gradle's docs explicitly warn: "Configuration-on-demand may not work well with plugins that modify the build model during configuration" (Multi-Project Builds).
“The first time we enabled both flags on CI, cache hit rate dropped from 92% to 41%. Switched off configure-on-demand, and it snapped back.”
— Senior build engineer at a fintech with 47 modular services, via a Gradle forum thread
Why does my CI build fail with `--configure-on-demand` but not locally?
This is the #1 support ticket I see. Local dev machines usually run the full module tree—you open the root project, Gradle evaluates everything, configuration order is stable. CI pipelines often use task-specific commands: `./gradlew :service:build` instead of `./gradlew build`. With configure-on-demand, Gradle only configures `:service` and its transitive project dependencies. The trick is that transitive means direct `project()` or `implementation project(':lib')` declared in dependencies. If your `:service` build script references a variable or extension from `:shared:config` via cross-project evaluation—like `rootProject.ext.myVersion`—and `:shared:config` isn't a declared dependency, configure-on-demand skips it. Result: property not found, build blows up. Locally, you probably ran a full build earlier that cached that value. The fix never changes: replace cross-project property access with a convention plugin or a version catalog. Or add `:shared:config` as an explicit dependency, even if no classes from it are used. That sounds wrong, but Gradle's own engineers suggest this in the Configuration On Demand docs. One concrete anecdote: we fixed a 30-minute debug session by scanning for `rootProject` and `parent` calls across 12 build scripts—six violations, all invisible without the flag.
So What Should You Do? A Simple Recap
For Small Projects (<10 modules)
Keep parallel execution on and skip configuration on-demand entirely. I have seen teams with five modules spend two days wiring up on-demand logic — only to shave 0.3 seconds off a build that originally took nine seconds. That's time you never get back. Your graph is flat, your configuration phase is cheap, and the overhead of tracking which tasks actually need configuration will outweigh any benefit. Just set org.gradle.parallel=true in your gradle.properties, test once, and move on. One caveat: if you share a CI runner with other projects, parallel mode can hog CPU cores. Cap it with org.gradle.workers.max=2 and you're fine.
For Medium Projects (10–50 modules)
This is where the trade-off bites. Most teams skip the hard part: they flip on both flags, see a green build, and ship it. Three weeks later someone’s incremental compile breaks because a transitive task dependency was silently skipped. The fix? Enable parallel execution first. Measure the win — it's usually 20–40% wall-clock improvement. Then add configuration on-demand only for the CI pipeline where you never run single-module tasks. For local development, keep it off until you hit a specific pain: "My IDE hangs for 15 seconds every time I change a line in module-b." That's the trigger — not a blog post telling you to turn it on by default.
Parallel cuts execution time; on-demand cuts configuration time. They solve different problems — don't assume they belong together.
— comment from a build engineer on a 34-module Android project, 2024
The risky combination is on-demand + heavy annotation processing. Each time Gradle re-evaluates which modules to configure, annotation processors may re-run, invalidating cached tasks. We fixed this by pinning the task graph after the first full build — not elegant, but stable. Wrong order? You lose a day to debugging why Room or Dagger suddenly regenerates files in the wrong order.
For Large Projects (50+ modules)
You have no choice: you need both, but you need them guarded. Configuration on-demand is practically mandatory here — waiting 90 seconds for Gradle to parse fifty build scripts just to run a single test is insane. However, run it without parallel mode and you serialize the entire execution phase. That hurts. The pattern I have seen work across three 60+ module codebases: enable parallel always, enable on-demand only on CI, and add a pre-commit hook that validates the task graph for the five most common command-line tasks. If `./gradlew :app:assembleDebug` tries to skip a necessary configuration, the hook fails the commit. One team called this the "seam blowout" check — because that's exactly what happens when you ship a build that silently skips module-b’s resources.
What usually breaks first is not Gradle itself but the human assumption that "configuration on-demand" means "zero configuration cost." It doesn't. It means deferred cost. The seam blows out when a developer adds a new module and forgets to register an explicit dependency — the build passes locally because on-demand configured only what it thought was needed, but CI runs a different task set. Catch that with a simple task-graph diff script before merging.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!