SYSONLINE
SDK36 · MIN 24
KOTLIN2.0
UTC22:22:55
UP00:01
MOD · ARTICLE · BUILD TOOLSS/N · AX-GRADLEPUBLISHED
All Articles
Build Tools · Updated 11 MIN

Gradle Build Optimization: Where Android Build Time Actually Goes

Inspect a slow Android build first, then fix it with the configuration cache, parallel execution, the build cache and a graph that lets Gradle skip work.

By Rocky Elsalaymeh · Founder & Principal Consultant, Strategia-X

Why Build Speed Matters More Than You Think

Gradle's own performance guide opens with the argument, and it needs no embellishment: "Build performance is essential to productivity. The longer a build takes, the more it disrupts your development flow. Since builds run many times a day, even small delays add up" (Gradle). The earlier version of this article promised to cut build times in half and said a few settings "often" save 30 to 50%. Neither number had a source, and both are gone, along with the old title. What Gradle does publish is a worked example. Its reference project, a small Java application and library, builds in 21 seconds as a baseline, and the guide reports the result of each change applied to it:
Change applied to Gradle's reference projectReported build time
Baseline, Gradle 8.1021 seconds
Updated to Gradle 8.138 seconds
Parallel execution7 seconds
Build cache5 seconds
Daemon3 seconds
Those are Gradle's numbers for Gradle's project. Yours will differ, which is the point of the next section. Everything here was checked against Gradle 9.8.1 and Android Gradle plugin 9.4.0, the current releases on 11 October 2026.

How do you find out where the time goes?

Gradle numbers this step zero: "Before making any changes, inspect your build with a Build Scan or profile report." Then it gives a loop that is worth following literally: "Inspect your build. Apply a change. Inspect your build again." Keep what helps and revert what does not. On Android there is a second instrument, the Build Analyzer in Android Studio, and the Android build guide attaches one concrete threshold to it: "If you observe slow builds, and in particular the garbage collection takes more than 15% of the build time in your Build Analyzer results, then you should increase the Java Virtual Machine (JVM) heap size." The earlier version of this article listed its own cut-offs here, a configuration phase over five seconds and a cache miss rate over 30%. They were invented. Read the scan for your build and act on the largest block of time, whatever it is.
bash
# A Build Scan: a web report of where the time went
./gradlew assembleDebug --scan

# A local profile report
./gradlew assembleDebug --profile

# The comparison that tells you whether incremental
# builds are working at all
./gradlew clean assembleDebug   # everything
./gradlew assembleDebug         # nothing changed

Configuration Cache and Parallel Execution

Four settings do most of the work. Three of them are off by default. - Parallel execution. "by default, Gradle runs only one task at a time". The caveat matters as much as the setting: "If a single subproject dominates execution time or there are many dependencies between subprojects, the benefits will be minimal." That is a module structure problem, covered below. - Build cache. "By default, Gradle does not use the Build Cache." - Configuration cache. The documentation now says "Since Gradle 9.0.0, the Configuration Cache is the preferred mode of execution" and "This feature will be enabled by default in Gradle 10". Until then you opt in. Beyond skipping the configuration phase, it "Executes all tasks in parallel, even within the same subproject." One limit to know before you promise anyone a faster sync: "IDE imports and syncs do not yet use the Configuration Cache." - Heap. Android's guidance is to "set the limit to 4, 6, or 8 gigabytes" and test. The earlier sample here carried four lines that should not be copied. 1. org.gradle.configuration-cache.problems=warn, left on permanently. Gradle's position: "Warning mode is a migration and troubleshooting aid and not intended as a persistent way of ignoring incompatibilities." 2. org.gradle.daemon=true. "Gradle enables the Daemon by default". 3. android.nonTransitiveRClass=true. Non-transitive R classes are worth having, and "This is the default behavior in Android Gradle plugin 8.0.0 and higher." 4. The android.defaults.buildfeatures.* flags. The AGP 9.0 release notes record the removal of the aidl, renderscript and buildconfig defaults flags.
properties
# gradle.properties

# Off by default: run tasks from different projects in parallel
org.gradle.parallel=true

# Off by default: reuse task outputs for the same inputs
org.gradle.caching=true

# Opt-in until Gradle 10
org.gradle.configuration-cache=true

# Heap for the Gradle daemon. Raise it only if the Build
# Analyzer shows garbage collection above 15% of build time.
org.gradle.jvmargs=-Xmx4g

# Not needed, do not add:
#   org.gradle.daemon=true              (already the default)
#   android.nonTransitiveRClass=true    (default since AGP 8.0)
#   org.gradle.configuration-cache.problems=warn
#       (a migration aid, not a setting to keep)

Dependency Management for Faster Resolution

Three habits here cost build time, and each has a documented fix. Dynamic versions. The earlier version of this article said a version such as 1.+ makes Gradle check the repositories on every build. That overstated it: "By default, Gradle caches dynamic versions for 24 hours". The real cost is broader, in Android's words: "Using dynamic version numbers can cause unexpected version updates, difficulty resolving version differences, and slower builds caused by Gradle checking for updates." Pin versions. A version catalog is the place to do it, and the version catalogs guide has a full one, which is why this article no longer carries its own stale copy. Too many repositories. Gradle: "Limit the number of repositories to the minimum required." Declare the one that serves most of your dependencies first. For an Android app that is google(). kapt. "The Kotlin Annotation Processing Tool (kapt) is significantly slower than the Kotlin Symbol Processor (KSP)." If a library you use supports KSP, move it.
kotlin
// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()        // serves most Android dependencies: first
        mavenCentral()
    }
}

// build.gradle.kts of a module that uses Room
dependencies {
    implementation(libs.room.runtime)
    ksp(libs.room.compiler)   // not kapt(...)
}

Module Structure and Build Avoidance

Settings cannot fix a dependency graph that forces recompilation. Two modules built in parallel only help if neither is waiting for the other, and a change only stays local if the modules above it cannot see it. The lever is implementation versus api. Gradle calls it out in the same guide: "Switching to implementation for internal-only dependencies is one of the most impactful changes you can make to improve build performance in large, modular codebases." The Java Library plugin documentation gives the mechanism: "less recompilations when implementation dependencies change: consumers would not need to be recompiled". Reserve api for a type that appears in the module's own public signatures. Everything else is implementation. How to draw the module boundaries in the first place is the subject of the modularization guide. The sample no longer applies the kotlin-android plugin. AGP 9 compiles Kotlin itself and rejects it.
kotlin
// core/model/build.gradle.kts
plugins {
    id("com.android.library")
}

dependencies {
    // api: Instant appears in this module's public signatures,
    // so consumers need it on their compile classpath
    api(libs.kotlinx.datetime)

    // implementation: invisible to consumers. A change here
    // does not recompile the modules that depend on this one.
    implementation(libs.kotlinx.serialization.json)
}

// feature/tasks/build.gradle.kts
dependencies {
    implementation(project(":core:model"))
    implementation(project(":data:tasks"))
}

CI-Specific Optimizations

A CI runner differs from a laptop in one way that matters: it usually starts cold. Gradle's daemon guidance reflects that: "On CI machines, long-lived agents benefit, but short-lived ones may not." On a hosted runner that is thrown away after each job, --no-daemon costs nothing. On a self-hosted agent that stays up, leave the daemon on. Caching is where CI time is won. The setup-gradle action restores and saves the Gradle caches, and its documentation describes the default: it "will only write to the cache from Jobs on the default (main/master) branch". Other branches read. The earlier sample set a cache-read-only expression to get that behaviour, which the action already has. The workflow below uses the current major versions of each action and gives the job a timeout, for the reasons in the GitHub Actions CI/CD guide.
yaml
# .github/workflows/android-ci.yml
jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v7

      - uses: actions/setup-java@v6
        with:
          distribution: zulu
          java-version: 17

      # Restores the Gradle caches. Writes them back only
      # from the default branch.
      - uses: gradle/actions/setup-gradle@v6

      - name: Build and test
        run: ./gradlew assembleDebug testDebugUnitTest --no-daemon

      - name: Upload build reports
        if: failure()
        uses: actions/upload-artifact@v7
        with:
          name: build-reports
          path: "**/build/reports/"
MOD · TAKEAWAYS6 POINTSSUMMARY

Key Takeaways

  1. 1Inspect the build with a Build Scan or profile report before changing anything, then change one thing and inspect again.
  2. 2Parallel execution and the build cache are off by default. The configuration cache is opt-in until Gradle 10.
  3. 3Do not keep configuration cache warning mode on. Gradle calls it a migration and troubleshooting aid.
  4. 4Pin dependency versions, keep the repository list short and replace kapt with KSP where the library supports it.
  5. 5Prefer implementation over api so a change inside a module does not recompile its consumers.
  6. 6On CI, let setup-gradle manage the cache and give every job a timeout.
MOD · FAQ3 ENTRIESANSWERED

Frequently Asked

Why is my Gradle configuration phase so slow?

The configuration phase runs on every build, whichever tasks you ask for, so anything expensive in a build script is paid each time. Enable the configuration cache so Gradle can skip the phase when its inputs are unchanged, apply plugins only where they are needed, and move shared logic into convention plugins.

Is the Gradle configuration cache enabled by default?

Not yet. The Gradle documentation says the configuration cache has been the preferred mode of execution since Gradle 9.0.0 and will be enabled by default in Gradle 10. Until then, set org.gradle.configuration-cache=true in gradle.properties. IDE imports and syncs do not use it, so it speeds up builds, not project sync.

Should I pass --no-daemon on CI?

It depends on the runner. Gradle states that on CI machines long-lived agents benefit from the daemon and short-lived ones may not. A hosted runner that is discarded after each job gains nothing from a warm daemon, so --no-daemon is reasonable there. A persistent self-hosted agent should keep the daemon enabled.

MOD · BUILD · NEXTS/N · AX-CTA-0001READY

Ready to architect your next Android app?

ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.

ANDROID-ARCHITECT · CONSOLE
S/N · AX-1A-00001
STRATEGIA-X · ENGINEERED · IN · CALIFORNIA