Gradle Configuration Cache: Cutting Configuration Time from Seconds to Milliseconds
Gradle Configuration Cache serializes the task graph after the first configuration, reducing subsequent configuration from seconds to ~150 ms. This blog shows how to enable it on a 3‑module Kotlin project, migrate to lazy APIs, and validate the cache. Trade‑offs and diagnostics are covered.
28 Aug 2026, 21:01 UTC

The Problem: Configuration Phase Tax
Every Gradle build pays a tax before it runs a single task. The configuration phase executes all build scripts, resolves plugins, and constructs the task graph. On a modest multi‑module Kotlin project, that tax runs 2–3 seconds on every invocation — even for help or a dry run. Multiply by dozens of daily builds per developer and CI pipelines, and the waste compounds.
Gradle 7.0 introduced a stable Configuration Cache that serializes the configured task graph after the first run. Subsequent builds reuse that serialized graph, skipping script execution entirely when inputs haven’t changed. The payoff: configuration drops from seconds to ~150 ms on a warm cache. The catch: your build logic must be side‑effect‑free during configuration, and many existing builds violate that constraint.
What the Cache Actually Stores
The Configuration Cache captures the result of the configuration phase — the fully resolved task graph with all task inputs, outputs, and dependencies calculated. It does not cache task execution outputs (that’s the separate Build Cache). On a cache hit, Gradle deserializes the graph and jumps straight to task execution.
Cache keys include: all build scripts (.gradle, .gradle.kts), applied plugins, gradle.properties, environment variables read via System.getenv() at configuration time, and the Gradle version. Any change invalidates the cache and triggers a fresh configuration pass.
Migration Requirements: Side‑Effect‑Free Configuration
To qualify for caching, configuration logic must not perform I/O, mutate global state, or depend on runtime values. Common blockers:
- Reading files with
project.file('version.txt').text - Accessing
configurations.resolvedConfiguration(forces resolution early) - Using eager APIs:
tasks.create,project.afterEvaluate,project.tasks.withTypethat configure immediately - Legacy plugins that execute code at configuration time (older
kotlin-android,realm,greendao)
Replace eager patterns with lazy APIs: tasks.register, tasks.withType<T>().configureEach { }, providers.gradleProperty('key'), providers.environmentVariable('NAME'). These defer work until the task graph is actually needed.
Worked Example: 3‑Module Kotlin/JVM Project
Structure: app, core, utils — each a Kotlin/JVM module using Kotlin DSL (.gradle.kts).
Step 1: Upgrade Gradle
Run on the project root (requires write access to gradle/wrapper/gradle-wrapper.properties):
./gradlew wrapper --gradle-version 8.8
Verify with ./gradlew --version (must show 8.x).
Step 2: Replace Eager Task Configuration
Before (in core/build.gradle.kts):
tasks.named<KotlinCompile>('compileKotlin') {
kotlinOptions.jvmTarget = '17'
}
After:
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.jvmTarget = '17'
}
configureEach registers a configuration action that runs lazily; tasks.named with eager configuration executes immediately.
Step 3: Lazy Version Property
Before (reads file at configuration time):
version = project.file('version.txt').readText().trim()
After (uses provider, read at execution time):
version = providers.gradleProperty('version')
.orElse(providers.fileContents(project.file('version.txt')).map { it.trim() })
Add version=1.2.3 to gradle.properties or keep the file; the provider reads it only when the version is actually used.
Step 4: Validate Cache Compatibility
Run from project root (no special permissions):
./gradlew help --configuration-cache
First run prints Configuration cache entry stored. Second run must print Configuration cache reused. If it prints Configuration cache could not be reused or fails, run with diagnostics:
./gradlew help --configuration-cache --configuration-cache-problems=verbose
This generates an HTML report at build/reports/configuration-cache/<timestamp>/configuration-cache-report.html listing exact incompatibilities.
Step 5: Measure the Difference
Profile before and after (run each twice, discard first):
./gradlew --profile help # before enabling cache
# add org.gradle.configuration-cache=true to gradle.properties
./gradlew --profile help # after, second run
Compare the Configuration phase duration in build/reports/profile/<timestamp>.html. Expect ~2.3 s → ~0.15 s on a warm cache for this project size.
Trade‑offs and Limitations
First build after any script change is slower. Cache invalidation + re‑serialization adds overhead (typically 10–30% over uncached configuration). Teams that edit build logic frequently (common in mono‑repos with shared convention plugins) see diminishing returns.
Remote build cache does not cache configuration phase. The Configuration Cache is local to each machine/agent. CI agents benefit only if they reuse workspaces or persist the .gradle/configuration-cache directory between runs.
Environment variables become cache inputs. Reading System.getenv('FOO') at configuration time bakes that value into the cache key. Changing FOO invalidates the cache. Prefer providers.environmentVariable('FOO') for lazy reading instead.
Kotlin DSL type‑checking runs during configuration. Heavy use of kotlin-dsl plugins with complex type resolution increases initial configuration time before caching pays off. This is a one‑time cost per cache entry.
Verification Checklist
./gradlew --version→ confirms Gradle 7.0+- Add
org.gradle.configuration-cache=truetogradle.properties ./gradlew help --configuration-cachetwice → second run showsConfiguration cache reused./gradlew --configuration-cache --dry-run assemble→ validates without side effects- Open
build/reports/configuration-cache/.../configuration-cache-report.htmlfor any remaining warnings - Benchmark with
./gradlew --profile helpbefore/after; compare Configuration phase
If step 3 fails, the HTML report identifies the exact line and plugin causing the incompatibility. Fix, then repeat from step 3.
Closing: Start Small, Measure, Expand
Enable Configuration Cache on a single module or a disposable branch first. The migration effort scales with build complexity — convention plugins and legacy third‑party plugins are the usual bottlenecks. Once green, the feedback loop tightens: ./gradlew test feels instant because the configuration tax disappears. For teams running hundreds of builds daily, that’s hours reclaimed per developer per week.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.