Build performance is critical to productivity. The longer builds take to complete, the more likely they’ll disrupt your development flow. Builds run many times a day, so even small waiting periods add up. The same is true for Continuous Integration (CI) builds: the less time they take, the faster you can react to new issues and the more often you can experiment.

All this means that it’s worth investing some time and effort into making your build as fast as possible. This section offers several ways to make a build faster. Additionally, you’ll find details about what leads to build performance degradation, and how you can avoid it.

Want faster Gradle Builds? Register here for our Build Cache training session to learn how Develocity can speed up builds by up to 90%.

Inspect your build

Before you make any changes, inspect your build with a build scan or profile report. A proper build inspection helps you understand:

  • how long it takes to build your project

  • which parts of your build are slow

Inspecting provides a comparison point to better understand the impact of the changes recommended on this page.

To best make use of this page:

  1. Inspect your build.

  2. Make a change.

  3. Inspect your build again.

If the change improved build times, make it permanent. If you don’t see an improvement, remove the change and try another.

Update versions


The Gradle team continuously improves the performance of Gradle builds. If you’re using an old version of Gradle, you’re missing out on the benefits of that work. Keeping up with Gradle version upgrades is low risk because the Gradle team ensures backwards compatibility between minor versions of Gradle. Staying up-to-date also makes transitioning to the next major version easier, since you’ll get early deprecation warnings.


Gradle runs on the Java Virtual Machine (JVM). Java performance improvements often benefit Gradle. For the best Gradle performance, use the latest version of Java.


Plugin writers continuously improve the performance of their plugins. If you’re using an old version of a plugin, you’re missing out on the benefits of that work. The Android, Java, and Kotlin plugins in particular can significantly impact build performance. Update to the latest version of these plugins for performance improvements.

Enable parallel execution

Most projects consist of more than one subproject. Usually, some of those subprojects are independent of one another; that is, they do not share state. Yet by default, Gradle only runs one task at a time. To execute tasks belonging to different subprojects in parallel, use the parallel flag:

$ gradle <task> --parallel

To execute project tasks in parallel by default, add the following setting to the file in the project root or your Gradle home:

Parallel builds can significantly improve build times; how much depends on your project structure and how many dependencies you have between subprojects. A build whose execution time is dominated by a single subproject won’t benefit much at all. Neither will a project with lots of inter-subproject dependencies. But most multi-subproject builds see a reduction in build times.

Visualize parallelism with build scans

Build scans give you a visual timeline of task execution. In the following example build, you can see long-running tasks at the beginning and end of the build:

parallel task slow
Figure 1. Bottleneck in parallel execution

Tweaking the build configuration to run the two slow tasks early on and in parallel reduces the overall build time from 8 seconds to 5 seconds:

parallel task fast
Figure 2. Optimized parallel execution

Re-enable the Gradle Daemon

The Gradle Daemon reduces build times by:

  • caching project information across builds

  • running in the background so every Gradle build doesn’t have to wait for JVM startup

  • benefiting from continuous runtime optimization in the JVM

  • watching the file system to calculate exactly what needs to be rebuilt before you run a build

Gradle enables the Daemon by default, but some builds override this preference. If your build disables the Daemon, you could see a significant performance improvement from enabling the daemon.

You can enable the Daemon at build time with the daemon flag:

$ gradle <task> --daemon

To enable the Daemon by default in older Gradle versions, add the following setting to the file in the project root or your Gradle home:

On developer machines, you should see a significant performance improvement. On CI machines, long-lived agents benefit from the Daemon. But short-lived machines don’t benefit much. Daemons automatically shut down on memory pressure in Gradle 3.0 and above, so it’s always safe to leave the Daemon enabled.

Enable the configuration cache

This feature has the following limitations:

  • The configuration cache does not support all core Gradle plugins and features. Full support is a work in progress.

  • Your build and the plugins you depend on might require changes to fulfill the requirements.

  • IDE imports and syncs do not use the configuration cache.

You can cache the result of the configuration phase by enabling the configuration cache. When build configuration inputs remain the same across builds, the configuration cache allows Gradle to skip the configuration phase entirely.

Build configuration inputs include:

  • Init scripts

  • Settings scripts

  • Build scripts

  • System properties used during the configuration phase

  • Gradle properties used during the configuration phase

  • Environment variables used during the configuration phase

  • Configuration files accessed using value suppliers such as providers

  • buildSrc inputs, including build configuration inputs and source files

By default, Gradle does not use the configuration cache. To enable the configuration cache at build time, use the configuration-cache flag:

$ gradle <task> --configuration-cache

To enable the configuration cache by default, add the following setting to the file in the project root or your Gradle home:

For more information about the configuration cache, check out the configuration cache documentation.

Additional configuration cache benefits

The configuration cache enables additional benefits as well. When enabled, Gradle:

  • Executes all tasks in parallel, even those in the same subproject.

  • Caches dependency resolution results.

Enable incremental build for custom tasks

Incremental build is a Gradle optimization that skips running tasks that have previously executed with the same inputs. If a task’s inputs and its outputs have not changed since the last execution, Gradle skips that task.

Most built-in tasks provided by Gradle work with incremental build. To make a custom task compatible with incremental build, specify the inputs and outputs:

tasks.register("processTemplatesAdHoc") {"engine", TemplateEngineType.FREEMARKER)
        .withPathSensitivity(PathSensitivity.RELATIVE)"", "docs")"templateData.variables", mapOf("year" to "2013"))

    doLast {
        // Process the templates here
tasks.register('processTemplatesAdHoc') {'engine', TemplateEngineType.FREEMARKER)
        .withPathSensitivity(PathSensitivity.RELATIVE)'', 'docs')'templateData.variables', [year: '2013'])

    doLast {
        // Process the templates here

For more information about incremental builds, check out the incremental build documentation.

Visualize incremental builds with build scan timelines

Look at the build scan timeline view to identify tasks that could benefit from incremental builds. This can also help you understand why tasks execute when you expect Gradle to skip them.

Figure 3. The timeline view can help with incremental build inspection

As you can see in the build scan above, the task was not up-to-date because one of its inputs ("timestamp") changed, forcing the task to re-run.

Sort tasks by duration to find the slowest tasks in your project.

Enable the build cache

The build cache is a Gradle optimization that stores task outputs for specific input. When you later run that same task with the same input, Gradle retrieves the output from the build cache instead of running the task again. By default, Gradle does not use the build cache. To enable the build cache at build time, use the build-cache flag:

$ gradle <task> --build-cache

To enable the build cache by default, add the following setting to the file in the project root or your Gradle home:

You can use a local build cache to speed up repeated builds on a single machine. You can also use a shared build cache to speed up repeated builds across multiple machines. Develocity provides one. Shared build caches can decrease build times for both CI and developer builds.

For more information about the build cache, check out the build cache documentation.

Visualize the build cache with build scans

Build scans can help you investigate build cache effectiveness. In the performance screen, the "Build cache" tab shows you statistics about:

  • how many tasks interacted with a cache

  • which cache was used

  • transfer and pack/unpack rates for these cache entries

cache performance
Figure 4. Inspecting the performance of the build cache for a build

The "Task execution" tab shows details about task cacheability. Click on a category to see a timeline screen that highlights tasks of that category.

task execution cacheable
Figure 5. A task oriented view of performance
timeline not cacheable
Figure 6. Timeline screen with 'not cacheable' tasks only

Sort by task duration on the timeline screen to highlight tasks with great time saving potential. The build scan above shows that :task1 and :task3 could be improved and made cacheable and shows why Gradle didn’t cache them.

Create builds for specific developer workflows

The fastest task is one that doesn’t execute. If you can find ways to skip tasks you don’t need to run, you’ll end up with a faster build overall.

If your build includes multiple subprojects, create tasks to build those subprojects independently. This helps you get the most out of caching, since a change to one subproject won’t force a rebuild for unrelated subprojects. And this helps reduce build times for teams that work on unrelated subprojects: there’s no need for front-end developers to build the back-end subprojects every time they change the front-end. Documentation writers don’t need to build front-end or back-end code even if the documentation lives in the same project as that code.

Instead, create tasks that match the needs of developers. You’ll still have a single task graph for the whole project. Each group of users suggests a restricted view of the task graph: turn that view into a Gradle workflow that excludes unnecessary tasks.

Gradle provides several features to create these workflows:

  • Assign tasks to appropriate groups

  • Create aggregate tasks: tasks with no action that only depend on other tasks, such as assemble

  • Defer configuration via gradle.taskGraph.whenReady() and others, so you can perform verification only when it’s necessary

Increase the heap size

By default, Gradle reserves 512MB of heap space for your build. This is plenty for most projects. However, some very large builds might need more memory to hold Gradle’s model and caches. If this is the case for you, you can specify a larger memory requirement. Specify the following property in the file in your project root or your Gradle home:

To learn more, check out the JVM memory configuration documentation.

Optimize Configuration

As described in the build lifecycle chapter, a Gradle build goes through 3 phases: initialization, configuration, and execution. Configuration code always executes regardless of the tasks that run. As a result, any expensive work performed during configuration slows down every invocation. Even simple commands like gradle help and gradle tasks.

The next few subsections introduce techniques that can reduce time spent in the configuration phase.

You can also enable the configuration cache to reduce the impact of a slow configuration phase. But even machines that use the cache still occasionally execute your configuration phase. As a result, you should make the configuration phase as fast as possible with these techniques.

Avoid expensive or blocking work

You should avoid time-intensive work in the configuration phase. But sometimes it can sneak into your build in non-obvious places. It’s usually clear when you’re encrypting data or calling remote services during configuration if that code is in a build file. But logic like this is more often found in plugins and occasionally custom task classes. Any expensive work in a plugin’s apply() method or a tasks’s constructor is a red flag.

Only apply plugins where they’re needed

Every plugin and script that you apply to a project adds to the overall configuration time. Some plugins have a greater impact than others. That doesn’t mean you should avoid using plugins, but you should take care to only apply them where they’re needed. For example, it’s easy to apply plugins to all subprojects via allprojects {} or subprojects {} even if not every project needs them.

In the above build scan example, you can see that the root build script applies the script-a.gradle script to 3 subprojects inside the build:

script a application
Figure 7. Showing the application of script-a.gradle to the build

This script takes 1 second to run. Since it applies to 3 subprojects, this script cumulatively delays the configuration phase by 3 seconds. In this situation, there are several ways to reduce the delay:

  • If only one subproject uses the script, you could remove the script application from the other subprojects. This reduces the configuration delay by two seconds in each Gradle invocation.

  • If multiple subprojects, but not all, use the script, you could refactor the script and all surrounding logic into a custom plugin located in buildSrc. Apply the custom plugin to only the relevant subprojects, reducing configuration delay and avoiding code duplication.

Statically compile tasks and plugins

Plugin and task authors often write Groovy for its concise syntax, API extensions to the JDK, and functional methods using closures. But Groovy syntax comes with the cost of dynamic interpretation. As a result, method calls in Groovy take more time and use more CPU than method calls in Java or Kotlin.

You can reduce this cost with static Groovy compilation: add the @CompileStatic annotation to your Groovy classes when you don’t explicitly require dynamic features. If you need dynamic Groovy in a method, add the @CompileDynamic annotation to that method.

Alternatively, you can write plugins and tasks in a statically compiled language such as Java or Kotlin.

Warning: Gradle’s Groovy DSL relies heavily on Groovy’s dynamic features. To use static compilation in your plugins, switch to Java-like syntax.

The following example defines a task that copies files without dynamic features:

project.tasks.register('copyFiles', Copy) { Task t ->

This example uses the register() and getByName() methods available on all Gradle “domain object containers”. Domain object containers include tasks, configurations, dependencies, extensions, and more. Some collections, such as TaskContainer, have dedicated types with extra methods like create, which accepts a task type.

When you use static compilation, an IDE can:

  • quickly show errors related to unrecognised types, properties, and methods

  • auto-complete method names

Optimize Dependency resolution

Dependency resolution simplifies integrating third-party libraries and other dependencies into your projects. Gradle contacts remote servers to discover and download dependencies. You can optimize the way you reference dependencies to cut down on these remote server calls.

Avoid unnecessary and unused dependencies

Managing third-party libraries and their transitive dependencies adds a significant cost to project maintenance and build times.

Watch out for unused dependencies: when a third-party library stops being used by isn’t removed from the dependency list. This happens frequently during refactors. You can use the Gradle Lint plugin to identify unused dependencies.

If you only use a small number of methods or classes in a third-party library, consider:

  • implementing the required code yourself in your project

  • copying the required code from the library (with attribution!) if it is open source

Optimize repository order

When Gradle resolves dependencies, it searches through each repository in the declared order. To reduce the time spent searching for dependencies, declare the repository hosting the largest number of your dependencies first. This minimizes the number of network requests required to resolve all dependencies.

Minimize repository count

Limit the number of declared repositories to the minimum possible for your build to work.

If you’re using a custom repository server, create a virtual repository that aggregates several repositories together. Then, add only that repository to your build file.

Minimize dynamic and snapshot versions

Dynamic versions (e.g. “2.+”), and changing versions (snapshots) force Gradle to contact remote repositories to find new releases. By default, Gradle only checks once every 24 hours. But you can change this programmatically with the following settings:

  • cacheDynamicVersionsFor

  • cacheChangingModulesFor

If a build file or initialization script lowers these values, Gradle queries repositories more often. When you don’t need the absolute latest release of a dependency every time you build, consider removing the custom values for these settings.

Find dynamic and changing versions with build scans

You can find all dependencies with dynamic versions via build scans:

dependency dynamic versions
Figure 8. Find dependencies with dynamic versions

You may be able to use fixed versions like "1.2" and "3.0.3.GA" that allow Gradle to cache versions. If you must use dynamic and changing versions, tune the cache settings to best meet your needs.

Avoid dependency resolution during configuration

Dependency resolution is an expensive process, both in terms of I/O and computation. Gradle reduces the required network traffic through caching. But there is still a cost. Gradle runs the configuration phase on every build. If you trigger dependency resolution during the configuration phase, every build pays that cost.

Switch to declarative syntax

If you evaluate a configuration file, your project pays the cost of dependency resolution during configuration. Normally tasks evaluate these files, since you don’t need the files until you’re ready to do something with them in a task action. Imagine you’re doing some debugging and want to display the files that make up a configuration. To implement this, you might inject a print statement:

tasks.register<Copy>("copyFiles") {
    println(">> Compilation deps: ${configurations.compileClasspath.get() { }}")
tasks.register('copyFiles', Copy) {
    println ">> Compilation deps: ${}"

The files property forces Gradle to resolve the dependencies. In this example, that happens during the configuration phase. Because the configuration phase runs on every build, all builds now pay the performance cost of dependency resolution. You can avoid this cost with a doFirst() action:

tasks.register<Copy>("copyFiles") {
    // Store the configuration into a variable because referencing the project from the task action
    // is not compatible with the configuration cache.
    val compileClasspath: FileCollection = configurations.compileClasspath.get()
    doFirst {
        println(">> Compilation deps: ${ { }}")
tasks.register('copyFiles', Copy) {
    // Store the configuration into a variable because referencing the project from the task action
    // is not compatible with the configuration cache.
    FileCollection compileClasspath = configurations.compileClasspath
    doFirst {
        println ">> Compilation deps: ${}"

Note that the from() declaration doesn’t resolve the dependencies because you’re using the dependency configuration itself as an argument, not the files. The Copy task resolves the configuration itself during task execution.

Visualize dependency resolution with build scans

The "Dependency resolution" tab on the performance page of a build scan shows dependency resolution time during the configuration and execution phases:

bad dependency resolution
Figure 9. Dependency resolution at configuration time

Build scans provide another means of identifying this issue. Your build should spend 0 seconds resolving dependencies during "project configuration". This example shows the build resolves dependencies too early in the lifecycle. You can also find a "Settings and suggestions" tab on the "Performance" page. This shows dependencies resolved during the configuration phase.

Remove or improve custom dependency resolution logic

Gradle allows users to model dependency resolution in the way that best suits them. Simple customizations, such as forcing specific versions of a dependency or substituting one dependency for another, don’t have a big impact on dependency resolution times. More complex customizations, such as custom logic that downloads and parses POMs, can slow down dependency resolution signficantly.

Use build scans or profile reports to check that custom dependency resolution logic doesn’t adversely affect dependency resolution times. This could be custom logic you have written yourself, or it could be part of a plugin.

Remove slow or unexpected dependency downloads

Slow dependency downloads can impact your overall build performance. Several things could cause this, including a slow internet connection or an overloaded repository server. On the "Performance" page of a build scan, you’ll find a "Network Activity" tab. This tab lists information including:

  • the time spent downloading dependencies

  • the transfer rate of dependency downloads

  • a list of downloads sorted by download time

In the following example, two slow dependency downloads took 20 and 40 seconds and slowed down the overall performance of a build:

slow dependency downloads
Figure 10. Identify slow dependency downloads

Check the download list for unexpected dependency downloads. For example, you might see a download caused by a dependency using a dynamic version.

Eliminate these slow or unexpected downloads by switching to a different repository or dependency.

Optimize Java projects

The following sections apply only to projects that use the java plugin or another JVM language.

Optimize tests

Projects often spend much of their build time testing. These could be a mixture of unit and integration tests. Integration tests usually take longer. Build scans can help you identify the slowest tests. You can then focus on speeding up those tests.

tests longest
Figure 11. Tests screen, with tests by project, sorted by duration

The above build scan shows an interactive test report for all projects in which tests ran.

Gradle has several ways to speed up tests:

  • Execute tests in parallel

  • Fork tests into multiple processes

  • Disable reports

Let’s look at each of these in turn.

Execute tests in parallel

Gradle can run multiple test cases in parallel. To enable this feature, override the value of maxParallelForks on the relevant Test task. For the best performance, use some number less than or equal to the number of available CPU cores:

tasks.withType<Test>().configureEach {
    maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
tasks.withType(Test).configureEach {
    maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1

Tests in parallel must be independent. They should not share resources such as files or databases. If your tests do share resources, they could interfere with each other in random and unpredictable ways.

Fork tests into multiple processes

By default, Gradle runs all tests in a single forked VM. If there are a lot of tests, or some tests that consume lots of memory, your tests may take longer than you expect to run. You can increase the heap size, but garbage collection may slow down your tests.

Alternatively, you can fork a new test VM after a certain number of tests have run with the forkEvery setting:

tasks.withType<Test>().configureEach {
    forkEvery = 100
tasks.withType(Test).configureEach {
    forkEvery = 100
Forking a VM is an expensive operation. Setting too small a value here slows down testing.

Disable reports

Gradle automatically creates test reports regardless of whether you want to look at them. That report generation slows down the overall build. You may not need reports if:

  • you only care if the tests succeeded (rather than why)

  • you use build scans, which provide more information than a local report

To disable test reports, set reports.html.required and reports.junitXml.required to false in the Test task:

tasks.withType<Test>().configureEach {
    reports.html.required = false
    reports.junitXml.required = false
tasks.withType(Test).configureEach {
    reports.html.required = false
    reports.junitXml.required = false
Conditionally enable reports

You might want to conditionally enable reports so you don’t have to edit the build file to see them. To enable the reports based on a project property, check for the presence of a property before disabling reports:

tasks.withType<Test>().configureEach {
    if (!project.hasProperty("createReports")) {
        reports.html.required = false
        reports.junitXml.required = false
tasks.withType(Test).configureEach {
    if (!project.hasProperty("createReports")) {
        reports.html.required = false
        reports.junitXml.required = false

Then, pass the property with -PcreateReports on the command line to generate the reports.

$ gradle <task> -PcreateReports

Or configure the property in the file in the project root or your Gradle home:

Optimize the compiler

The Java compiler is fast. But if you’re compiling hundreds of Java classes, even a short compilation time adds up. Gradle offers a several optimizations for Java compilation:

  • Run the compiler as a separate process

  • Switch internal-only dependencies to implementation visibility

Run the compiler as a separate process

You can run the compiler as a separate process with the following configuration for any JavaCompile task:

<task>.options.isFork = true
<task>.options.fork = true

To apply the configuration to all Java compilation tasks, you can configureEach java compilation task:

tasks.withType<JavaCompile>().configureEach {
    options.isFork = true
tasks.withType(JavaCompile).configureEach {
    options.fork = true

Gradle reuses this process within the duration the build, so the forking overhead is minimal. By forking memory-intensive compilation into a separate process, we minimize garbage collection in the main Gradle process. Less garbage collection means that Gradle’s infrastructure can run faster, especially when you also use parallel builds.

Forking compilation rarely impacts the performance of small projects. But you should consider it if a single task compiles more than a thousand source files together.

Switch internal-only dependencies to implementation visibility

Only libraries can define api dependencies. Use the java-library plugin to define API dependencies in your libraries. Projects that use the java plugin cannot declare api dependencies.

Before Gradle 3.4, projects declared dependencies using the compile configuration. This exposed all of those dependencies to downstream projects. In Gradle 3.4 and above, you can separate downstream-facing api dependencies from internal-only implementation details. Implementation dependencies don’t leak into the compile classpath of downstream projects. When implementation details change, Gradle only recompiles api dependencies.

dependencies {
dependencies {
   api project('my-utils')
   implementation ''

This can significantly reduce the "ripple" of recompilations caused by a single change in large multi-project builds.

Improve the performance of older Gradle releases

Some projects cannot easily upgrade to a current Gradle version. While you should always upgrade Gradle to a recent version when possible, we recognize that it isn’t always feasible for certain niche situations. In those select cases, check out these recommendations to optimize older versions of Gradle.

Enable the Daemon

Gradle 3.0 and above enable the Daemon by default. If you are using an older version, you should update to the latest version of Gradle. If you cannot update your Gradle version, you can enable the Daemon manually.

Use incremental compilation

Gradle can analyze dependencies down to the individual class level to recompile only the classes affected by a change. Gradle 4.10 and above enable incremental compilation by default. To enable incremental compilation by default in older Gradle versions, add the following setting to your build.gradle file:

tasks.withType<JavaCompile>().configureEach {
    options.isIncremental = true
tasks.withType(JavaCompile).configureEach {
    options.incremental = true

Use compile avoidance

Often, updates only change internal implementation details of your code, like the body of a method. These updates are known as ABI-compatible changes: they have no impact on the binary interface of your project. In Gradle 3.4 and above, ABI-compatible changes no longer trigger recompiles of downstream projects. This especially improves build times in large multi-project builds with deep dependency chains.

Upgrade to a Gradle version above 3.4 to benefit from compile avoidance.

If you use annotation processors, you need to explicitly declare them in order for compilation avoidance to work. To learn more, check out the compile avoidance documentation.

Optimize Android projects

Everything on this page applies to Android builds, since Android builds use Gradle. Yet Android introduces unique opportunities for optimization. For more information, check out the Android team performance guide. You can also watch the accompanying talk from Google IO 2017.