Faster Gradle builds: the fixes that actually help
Measure first, then fix: the Gradle and Android settings that really shorten builds, the changes that take a day, and the popular tricks that barely matter.

The worst build I ever sat through was not the slowest one. It was the one that took minutes after I changed a single string, because I knew how long a string change ought to take and the build did not care. Most slow Android builds are like that. They are not slow everywhere. They are slow in one place, for one reason, every single time.
That is good news, because one reason can be found. The bad news is that most advice about Gradle speed is folklore, copied from one gradle.properties file to the next like a family recipe nobody dares to touch. This piece sorts the fixes that help from the ones that only feel productive, based on the Gradle and Android Gradle plugin releases current in autumn 2026. The method is boring on purpose: measure, change one thing, measure again.
Contains affiliate links.
Measure before you touch anything
Pick the build you actually run dozens of times a day. For most Android developers that is an incremental debug build of the app after a small change in one module, not a clean build. Clean builds matter on CI, but optimizing them on a laptop is like training for a marathon by practicing the start.
Three tools cover nearly everything.
- Build Analyzer in Android Studio. After a build, open the Build window and switch to the Build Analyzer tab. It focuses on the tasks and plugins that decide the total duration, and its warnings point at always-run tasks, non-incremental annotation processors, a configuration cache that is not enabled and a leftover Jetifier flag. On Windows it also lists folders to exclude from Microsoft Defender's real-time scanning, and on some machines that alone is the biggest win in this article.
- A Build Scan or a profile report. Both come from the command line. The scan is richer and shows a timeline, cache hits and dependency downloads, but it is published to Gradle's Build Scan service and you may be asked to agree to terms first, so check that with whoever owns the code. The profile report stays local, as HTML in
build/reports/profile. - Gradle Profiler, a separate open-source tool from Gradle, for comparing two settings properly. It warms up the daemon, runs the same scenario several times and can apply a small source change between runs, which is exactly the "I changed one function" build you care about.
./gradlew :app:assembleDebug --scan
./gradlew :app:assembleDebug --profile
gradle-profiler --benchmark --scenario-file perf.scenarios small_change
The scenario file is short:
small_change {
tasks = [":app:assembleDebug"]
apply-non-abi-change-to = "feature/profile/src/main/kotlin/ProfileScreen.kt"
warm-ups = 3
iterations = 5
}
If the Kotlin compiler is where the time goes, add kotlin.build.report.output=file to gradle.properties. The Kotlin plugin then writes a report under build/reports/kotlin-build that shows what it compiled, which is how you find out that a one-line change triggered a full recompilation of a module and why. Keep a plain text file with a before and after number for every change you try. You will want it the first time someone asks whether that heap setting was really necessary.
The one-line settings
These go in gradle.properties at the root of the project. In current Gradle releases the build cache, parallel execution and the configuration cache are all switched off by default; only the daemon is on. A reasonable starting point:
org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
Parallel execution
This lets Gradle run tasks from different modules at the same time. It helps in proportion to how many independent modules you have. A project with one big app module has very little to run side by side, which is a hint for the next section.
The build cache
The build cache stores task outputs keyed by their inputs and reuses them when the same inputs come back. On a laptop it pays off when you switch branches, or rebuild something you built yesterday. A shared remote cache, filled by CI and read by every laptop, helps a team much more, but it needs a server. Gradle supports an HTTP cache backend out of the box and Develocity offers a cache node. Start with the local cache, check that tasks are actually marked FROM-CACHE in the scan, and only then think about a remote one.
The configuration cache
Before Gradle runs a single task it configures every project involved. In a build with many modules and plugins, that configuration phase is often the part of a small build you feel most. The configuration cache stores the result and skips the whole phase when nothing that affects it has changed; it also lets tasks run in parallel even within one module. The first run prints "Calculating task graph as no configuration cache is available", later ones "Reusing configuration cache".
Not every plugin is compatible, and Build Analyzer tells you about yours before you flip the switch. The Android documentation suggests org.gradle.configuration-cache.problems=warn while migrating. Gradle's own documentation is cooler on warning mode, since it can also let new incompatibilities slip in unnoticed. Use it for a week, fix what it reports, remove it. One more thing: Android Studio's project sync does not use the configuration cache, so a slow sync is a separate problem with separate causes.
Heap
The Gradle daemon's default heap is 512 MB, which no Android project I have seen fits into. The Android docs suggest trying 4, 6 and 8 gigabytes and keeping whatever measures best. Don't simply max it out. Memory you give Gradle is memory the emulator and Android Studio no longer have, and on a 16 GB laptop that trade gets ugly quickly. The Kotlin daemon inherits the Gradle daemon's JVM arguments unless you set kotlin.daemon.jvmargs separately.
Old flags that switch new defaults off
Open gradle.properties and read it line by line. Non-transitive R classes have been the default since Android Gradle plugin 8.0, and Jetifier is off by default in current versions. A project that went through a migration years ago may still carry android.nonTransitiveRClass=false or android.enableJetifier=true, each of them quietly making builds slower. The same goes for build features: recent plugin versions no longer generate BuildConfig or resource values unless you ask, so switch those on in the modules that need them rather than for the whole project. Delete the leftovers, fix what breaks, measure.
The changes that take a day or more
From kapt to KSP
The Android build speed documentation is blunt about it: kapt is significantly slower than KSP. kapt has to generate Java stubs from your Kotlin code before annotation processors can even start; KSP reads Kotlin directly. Room supports KSP, as do most popular processors, so check each dependency that still uses kapt. A single remaining kapt processor keeps the whole stub generation step in your build, so the win only arrives when the last one is gone.
Split the app module
Gradle skips, caches and parallelizes work per task and per module. When everything lives in one module, a change anywhere can recompile a lot of code. With feature modules, a change in feature/profile rebuilds that module and whatever depends on it. Prefer implementation over api for dependencies between modules; Gradle's documentation points out that this reduces cascading recompilations, because modules that never used a dependency directly don't have to care when it changes.
Don't split for sport, though. Every module adds some configuration work. A handful of feature modules depending on a few core modules is a good shape. A tangled web of dozens of tiny modules is a different hobby.
Keep debug builds static
Version codes counted from Git commits, build timestamps written into code, resources generated from the date: each of these changes an input on every build, and every task downstream runs again. The Android documentation recommends keeping such dynamic values in release variants only. Use fixed dependency versions too. A dynamic version like 2.+ makes Gradle ask repositories whether something newer exists, and it makes builds less reproducible while it is at it. While you are in settings.gradle.kts, the Android docs also suggest trying the Gradle Plugin Portal last in the repository list, so Gradle doesn't search it for every library first.
Tricks that help less than you'd hope
- Running clean before every build. It throws away exactly what incremental compilation and the caches saved for you.
- Switching off the daemon to save memory. Every build then pays for a cold JVM.
- Offline mode as a habit. It only helps when Gradle keeps checking the network, which usually means dynamic versions somewhere.
- Copying someone's gradle.properties, this one included, without measuring it on your own project.
- Buying a faster laptop before you know what the current one spends its build time on.
Which fix to try first
| Fix | What it speeds up | Effort | How you know it worked |
|---|---|---|---|
| Antivirus exclusions on Windows | Every build | Minutes | Build Analyzer warning disappears |
| Heap size | Builds close to the memory limit | Minutes | Less garbage collection time in the scan |
| Local build cache | Branch switches, repeated builds | Minutes | Tasks marked FROM-CACHE |
| Parallel execution | Projects with several modules | Minutes | Overlapping tasks in the scan timeline |
| Remove old flags | Depends on the flag | An hour | Fewer Build Analyzer warnings |
| Static debug values | Incremental builds | An hour or two | Fewer tasks run after a no-op build |
| Configuration cache | Small builds in big projects | Hours to days | "Reusing configuration cache" |
| kapt to KSP | Every build with processors | About a day per processor | Kotlin build report |
| Feature modules | Incremental builds | Weeks | Fewer tasks after a small change |
Faster builds also make every other loop bearable: the JVM screenshot tests from the snapshot testing field guide are ordinary unit test tasks and benefit from the cache like everything else, and the install script in the guide to a small Android test device lab starts with a build too. If you are new enough to Gradle that its first build ate half an evening, the Android for beginners plan explains that this is normal, and the rest of this article can wait a few weekends.
For everyone else, start with the cheapest test there is. Run the same debug build twice in a row without changing anything. The second run should do almost nothing. Every task that still executes is a task with an input that changes on its own, and that list, not a new gradle.properties from the internet, is where Monday morning begins. More build notes live on the mobile development track.
Keep reading

A powered USB hub for test phones: power, ports and switches
How to choose a powered USB hub for test phones: why bus-powered hubs drop devices, what to read on the spec sheet, and why a switch on every port helps.

USB-C hub for MacBook: what to check before you buy, and picks
What to check in a USB-C hub for a MacBook: pass-through power, how many screens macOS will extend, USB-A for test phones, card readers, Ethernet and heat.

A mobile developer's desk: hubs, cables, chargers and stands
What a mobile developer's desk needs: USB-C hubs and specs that matter, data versus charging cables, multi-port chargers, phone stands and a second screen.