How MSBuild Incremental Build Works and Why Targets Re-Execute Unnecessarily

MSBuild incremental build skips targets when all output files are newer than their corresponding inputs, but targets re-execute unnecessarily when Inputs/Outputs attributes are missing, volatile data changes output paths, or generated files aren't registered in FileWrites.

The dotnet/skills repository provides comprehensive technical guidance on MSBuild incremental build mechanisms within the plugins/dotnet-msbuild/skills/incremental-build/SKILL.md file. Understanding how MSBuild determines whether to skip or execute targets is essential for optimizing build performance in large .NET projects, as unnecessary target execution can significantly increase build times.

How MSBuild Incremental Build Works

MSBuild determines whether a target can be skipped by comparing the last-write timestamps of files listed in the target's Inputs and Outputs attributes. If every output file is newer than every input file, the target is considered up-to-date and is omitted from the build. This timestamp-based comparison is the core mechanism of MSBuild's incremental build feature.

Targets can explicitly control this behavior using the Incremental attribute. When set to Incremental="false", the target always runs regardless of timestamps. When set to Incremental="true", MSBuild strictly enforces the timestamp comparison. According to the source documentation in SKILL.md, this comparison is based solely on file timestamps, not content hashes, meaning touching a file (updating its timestamp without changing content) forces a rebuild.

Common Causes of Unnecessary Target Execution

Targets re-execute unnecessarily due to several common pitfalls in MSBuild project configuration. The dotnet/skills analysis identifies specific patterns that break incrementality.

Missing Inputs and Outputs Attributes

The single most common cause of targets always running is the absence of both Inputs and Outputs attributes on custom targets. Without these attributes, MSBuild has no mechanism to determine whether the target is up-to-date.

<!-- Target that always runs (missing Inputs/Outputs) -->
<Target Name="PrintMessage">
  <Message Text="This runs every build" />
</Target>

Volatile Data in Output Paths

When output paths contain volatile data such as timestamps, GUIDs, or random values, the output files appear "missing" on subsequent builds because the path changes. This forces MSBuild to treat the target as out-of-date.

Unregistered Generated Files

Files written by a target that are not listed in the Outputs attribute or the FileWrites item group cause incremental build failures. Generated files must be registered in FileWrites so that dotnet clean removes them and incremental checks remain accurate.

<Target Name="GenerateConfig"
        Inputs="$(MSBuildProjectFile);@(ConfigInput)"
        Outputs="$(IntermediateOutputPath)config.generated.cs"
        BeforeTargets="CoreCompile">
  <WriteLinesToFile File="$(IntermediateOutputPath)config.generated.cs" Lines="..." />
  <ItemGroup>
    <FileWrites Include="$(IntermediateOutputPath)config.generated.cs" />
    <Compile Include="$(IntermediateOutputPath)config.generated.cs" />
  </ItemGroup>
</Target>

Visual Studio Fast Up-To-Date Check Conflicts

Visual Studio performs its own Fast Up-To-Date Check (FUTDC) that can become out-of-sync with MSBuild when custom targets generate files that FUTDC does not know about. This causes builds within Visual Studio to behave differently than command-line builds.

Disable FUTDC to force Visual Studio to rely on MSBuild's incremental logic:

<PropertyGroup>
  <DisableFastUpToDateCheck>true</DisableFastUpToDateCheck>
</PropertyGroup>

Diagnosing Unnecessary Rebuilds

To diagnose why targets re-execute, use binary logs produced by the /bl:first.binlog and /bl:second.binlog command-line arguments. Search the logs for specific messages such as "Building target completely", "Building target incrementally", or "Skipping target".

The binlog analysis tools, referenced in eng/skill-validator/src/Evaluate/SessionDatabase.cs, can pinpoint the exact input file that triggered a rebuild by comparing file hashes and timestamps across build sessions.

Implementing Proper Incremental Build Targets

Correctly configured targets declare their dependencies explicitly and register all outputs.

A minimal incremental target includes both Inputs and Outputs:

<Target Name="Transform"
        Inputs="@(TransformFiles)"
        Outputs="$(IntermediateOutputPath)%(Filename).out">
  <!-- Transformation work -->
</Target>

To explicitly force execution regardless of timestamps, use the Incremental attribute:

<Target Name="AlwaysRun" Incremental="false"
        Inputs="@(Compile)" Outputs="obj\always.txt">
  <Message Text="Forced execution" />
</Target>

Summary

  • MSBuild incremental build compares timestamps of Inputs against Outputs to determine if targets can be skipped.
  • Missing Inputs/Outputs is the most common reason targets execute unnecessarily.
  • Volatile output paths and unregistered generated files break incrementality by making outputs appear missing or stale.
  • Visual Studio FUTDC can conflict with MSBuild incrementality; disable it with DisableFastUpToDateCheck.
  • Binary logs provide precise diagnostics for identifying which input changes trigger rebuilds.
  • FileWrites registration ensures generated files are tracked for both incremental checks and clean operations.

Frequently Asked Questions

What makes a target incremental in MSBuild?

A target becomes incremental when it declares both Inputs and Outputs attributes. MSBuild compares the timestamps of all input files against all output files; if every output is newer than every input, the target skips execution. Targets can also use the Incremental="true" attribute to explicitly opt into this behavior.

Why does my custom target run every time even when nothing changed?

Your custom target likely lacks Inputs and Outputs attributes, or the output paths contain volatile data (timestamps, GUIDs) that change between builds. Another common cause is failing to register generated files in the FileWrites item group, causing MSBuild to lose track of what the target produced.

How do I disable Visual Studio's Fast Up-To-Date Check?

Add the DisableFastUpToDateCheck property to your project file:

<PropertyGroup>
  <DisableFastUpToDateCheck>true</DisableFastUpToDateCheck>
</PropertyGroup>

This forces Visual Studio to rely on MSBuild's incremental build logic instead of its own heuristic, ensuring consistency between IDE and command-line builds.

What is the FileWrites item group used for?

The FileWrites item group registers files generated during the build so that dotnet clean can remove them and subsequent incremental checks can account for their existence. Files written by custom targets must be added to this group to prevent stale file detection and ensure accurate up-to-date checks.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →