skip to content

How does choosing `tasks.register` over `tasks.create` affect the configuration phase?

level: middleimportance: must knowfreq 65%

answer

  1. create = eager realization at config time
  2. register = deferred until realized
  3. TaskProvider, pay-as-you-go
  4. configuration avoidance
  5. .get()/forEach can force realization

basics

~20 s

tasks.create eagerly builds and configures the task during the configuration phase, even if it's never run. tasks.register defers that work until the task is actually needed, so unused tasks cost almost nothing at configuration time.

solid answer

~40 s

During configuration, Gradle evaluates every build script, so the cost of how you declare tasks is paid on every build. `tasks.create("x") { ... }` **eagerly** instantiates the task object and runs its configuration block immediately — whether or not `x` is in the requested graph. `tasks.register("x") { ... }` returns a `TaskProvider` and **defers** both creation and configuration until something actually realizes the task (it's requested, or `.get()`/wiring forces it). In a large multi-project build where most tasks are never executed, switching to `register` (and the lazy `named`/Provider APIs) dramatically cuts configuration time, because Gradle skips realizing tasks nobody asked for. The register-vs-create API itself is the subject of a dedicated topic; here the key point is its *effect on the configuration phase*: `create` adds unconditional configuration-time work, `register` makes that work pay-as-you-go.

code

kotlin · 5 lines
kotlin
// Eager: configured every build even if never run
tasks.create("eager") { doLast { println("run") } }

// Lazy: configured only when realized
tasks.register("lazy") { doLast { println("run") } }

go deeper

for a junior

Know that register is lazy and create is eager, and that lazy is the default recommendation.

for a middle

Explain the realization timing precisely and that create configures unused tasks on every build; give a code example.

for a senior

Tie it to overall configuration-time budget and identify accidental-realization pitfalls (.get(), eager iteration).

for a principal

Mandate register/lazy APIs in shared convention plugins and reason about configuration-time scaling across a large multi-project build.

## The setup The configuration phase evaluates each `build.gradle.kts` top to bottom on every invocation. Any task you declare in that script contributes work to configuration unless that work is deferred. There are two ways to declare a task, and they differ precisely in *when* the work happens. ## Eager: `tasks.create` ```kotlin tasks.create("report") { // runs NOW, during configuration of this project, every build description = "Generates a report" dependsOn("compileJava") } ``` `create` immediately constructs the `Task` object and executes the configuration closure. This happens during the configuration phase **regardless of whether `report` is ever executed**. Across hundreds of tasks and many subprojects, this eager realization is a major source of slow configuration. ## Lazy: `tasks.register` ```kotlin val report = tasks.register("report") { description = "Generates a report" dependsOn("compileJava") } ``` `register` returns a `TaskProvider<Task>` and **does not** create the task or run its block yet. The task is **realized** (created + configured) only when needed — when it's part of the requested task graph, or when code calls `report.get()`, or when another lazy API resolves it. If nobody asks for `report` in this build, its configuration block never runs. This is part of Gradle's broader **configuration avoidance** strategy. ## Why this matters for the configuration phase specifically The phase's whole job is to build the task model cheaply enough that it can run on *every* command. `create` makes that impossible to keep cheap as the build grows, because it forces realization of tasks irrelevant to the current command. `register` keeps configuration cost proportional to what you actually use. ```kotlin // Anti-pattern: eager realization forces configuration work for unused tasks tasks.create("slowEager") { /* heavy setup */ } // Preferred: deferred — only realized if requested tasks.register("fastLazy") { /* heavy setup */ } ``` ## A subtle trap Calling `tasks.named("slowLazy").get()` or iterating the whole `TaskContainer` eagerly (e.g. `tasks.forEach { }` or `withType(T::class).all { }` in older patterns) can *re-realize* tasks you carefully registered lazily, undoing the benefit. Prefer `configureEach` / `withType(...).configureEach { }` which stay lazy. ## Boundary note The full `register` vs `create` API contract (return types, `TaskProvider` wiring) belongs to the task-declaration topic. The point relevant to the *configuration phase* is the timing: eager creation loads the configuration phase with work for unused tasks; deferred registration keeps that phase lean.

  • Name something that can accidentally realize a registered task at configuration time.
    Calling `.get()` on its TaskProvider, or eager container iteration like `tasks.forEach { }` / `withType(...).all { }`. Prefer `configureEach` to stay lazy.
  • Does `register` skip running the task body too?
    Task actions never run at configuration time regardless. What `register` defers is the task's creation and its *configuration block*; actions only ever run at execution if the task is in the graph.

saying these in an interview costs you the question

  • Claiming `register` runs the task action lazily — it defers configuration, not execution.
  • Saying `create` and `register` have identical performance.
  • Forgetting that eager container iteration can undo lazy registration.

context