skip to content

A task keeps re-running every build even though nothing seems to change. How do you diagnose why it's never UP-TO-DATE?

level: seniorimportance: should knowfreq 45%

answer

  1. --info prints the change reason
  2. build scan 'why did this run'
  3. timestamp/UUID in @Input
  4. output dir overlap/churn
  5. doNotTrackState as last resort

basics

~20 s

Find which declared input or output changes each run. Use --info to see Gradle's reason for re-running, or a build scan's timeline. Common causes: a timestamp/absolute path baked into an input, or an output written outside the declared location.

solid answer

~50 s

When a task is never UP-TO-DATE, an input or output fingerprint differs every run. The fastest path is `--info`, which logs a line like "Task ':x' is not up-to-date because: Input property 'y' has changed" or "Output property 'z' file ... has been removed." A **build scan** shows the same under the task's timeline with the exact changed property. Typical culprits: an `@Input` value that embeds a wall-clock timestamp or build-number, an `@InputFiles` set that includes generated files with changing content, absolute paths that vary by machine (a path-sensitivity concern), or a task whose output directory is also another task's working area so files appear/disappear. Once you know which property churns, you either make it deterministic, move it to `@Internal` if it shouldn't affect output, or fix the producing task. If non-determinism is unavoidable, `doNotTrackState("reason")` opts the task out of up-to-date checks entirely, but that means it always runs.

code

bash · 3 lines
bash
$ ./gradlew generateConfig --info | grep -A2 'not up-to-date'
Task ':generateConfig' is not up-to-date because:
  Input property 'buildTimestamp' has changed.

go deeper

for a junior

Know that --info shows why a task ran and that changing inputs cause re-runs.

for a middle

Use --info / build scan to find the churning property and name common causes like timestamps in inputs.

for a senior

Reason about output overlap, CI vs local differences, and choose between fixing determinism and doNotTrackState.

for a principal

Drive build-correctness standards: forbid non-deterministic tracked inputs, audit doNotTrackState usage, and standardize scan-based diagnosis in CI.

## Symptom: the task always executes A correctly declared task should be UP-TO-DATE on a no-op rebuild. If it isn't, by definition some declared input or output fingerprint changed between runs. Diagnosis is about finding **which** one. ## Step 1 — ask Gradle why Run with `--info`. Before each non-skipped task, Gradle prints the reason it decided to execute: ``` > Task :generateConfig Task ':generateConfig' is not up-to-date because: Input property 'buildTimestamp' has changed. ``` This names the exact property. Other reasons include "No history is available" (first run / cache cleaned), "Output property '...' file ... has changed", and "...has been removed." ## Step 2 — use a build scan for richer data `./gradlew build --scan` produces a timeline where each task's "Why did this task run?" panel lists the changed inputs/outputs and even diffs input property values. This is the best tool for intermittent or CI-only churn. ## Common root causes 1. **Non-deterministic input value.** An `@Input` holding `System.currentTimeMillis()`, a UUID, or a git short-hash changes every build. Fix: derive it from a stable source or move volatile-but-irrelevant values to `@Internal`. 2. **Changing input file content.** An upstream generator emits files with embedded timestamps; downstream tasks then re-run forever. Fix the generator's determinism. 3. **Output overlap / churn.** Two tasks write into the same directory, so each sees the other's files appear and disappear, breaking the output snapshot. Give each task a distinct output location. 4. **Absolute paths in inputs.** Encoding machine-specific paths makes fingerprints machine-specific (handled by path-sensitivity normalization, a sibling topic, but it shows up here as churn). 5. **Missing history.** A clean build or a wiped `.gradle` has no prior snapshot, so the first run always executes — expected, not a bug. ## Step 3 — the escape hatch If an input is legitimately non-deterministic and you cannot make it stable (e.g., a task that probes a live system), declaring it untrackable is honest: ```kotlin tasks.register("probe") { doNotTrackState("Reads a live external service; results are never reproducible") doLast { /* always run */ } } ``` This tells Gradle the task has no reliable up-to-date state, so it always runs (and is never cached). Use it sparingly — it disables the optimization rather than fixing it. ## Mental model The up-to-date decision is deterministic: same declared inputs ⇒ skip. "Always runs" therefore always reduces to "some declared fingerprint isn't stable." Find it, make it stable, or explicitly opt the task out.

  • Why might a task be out of date only on CI but UP-TO-DATE locally?
    CI often does a clean checkout (no .gradle history) or uses machine-specific absolute paths; the first means 'no history available', the second means input fingerprints differ per agent. A shared build cache plus path normalization addresses both.
  • When is doNotTrackState the right answer versus a workaround?
    It's right when the task's result is genuinely non-reproducible (probing live state). It's a workaround — and a smell — when used to mask a fixable non-deterministic input you could stabilize instead.
  • What does 'No history is available' as a reason mean?
    Gradle has no recorded snapshot from a previous run (first build, or .gradle state was cleaned), so it must execute and record fresh history.

saying these in an interview costs you the question

  • Reaching for doNotTrackState() before diagnosing the actual churning property.
  • Blaming the build cache when the issue is local up-to-date churn from a non-deterministic input.
  • Embedding build timestamps/UUIDs in tracked @Input properties and expecting UP-TO-DATE.

context