How MSBuild Incremental Builds Avoid Full Rebuilds in Visual Studio
MSBuild incremental builds skip targets whose outputs are newer than their inputs. Learn how the timestamp check works, what silently breaks it, and how to verify only changed projects recompile.
20 Aug 2025, 23:37 UTC

If a large solution takes minutes to rebuild after you change one file, something is wrong: MSBuild is designed to skip work it has already done. Incremental builds compare the timestamps of inputs (source files, references, resources) against outputs (compiled assemblies, copied content) and only re-run targets whose inputs are newer than their outputs. Understanding how that comparison works — and what silently breaks it — is the difference between a 10-second build and a 5-minute one.
The mechanism: inputs, outputs, and timestamps
Every MSBuild target can declare Inputs and Outputs attributes. Before running a target, MSBuild checks whether every output file is newer than every input file. If so, the target is skipped entirely. The core compile target in a C# project effectively maps all .cs files (plus referenced assemblies and project files) as inputs and the project's .dll as the output. Touch one source file, and only that project's compile step re-runs — assuming project references are set up correctly.
Project-to-project references matter here. When Project B references Project A via a <ProjectReference>, MSBuild knows B's output depends on A's output. If A rebuilds, B rebuilds too. If you instead reference A's DLL directly with a file <Reference>, that dependency chain is invisible to the build engine, and B can end up compiled against a stale binary.
A worked example you can inspect
Create or open a solution with two projects where an app references a library. Build once, then edit a single method in the library and build again with detailed verbosity. In Visual Studio, set Tools > Options > Projects and Solutions > Build and Run > MSBuild project build output verbosity to Detailed, or run from a Developer Command Prompt (no elevation needed):
msbuild MySolution.sln /v:d /t:BuildIn the output, look for lines like:
Skipping target "CoreCompile" because all output files are up-to-date with respect to the input files.That message is your confirmation that incremental logic is working. You should see the library project recompile while the unchanged projects report their targets skipped. If instead every project recompiles after a one-line edit, read on — something in your build is defeating the timestamp check.
What silently breaks incremental builds
The most common culprits, in rough order of frequency:
- File references instead of project references. As noted above, direct DLL references hide dependencies. Fix: replace them with
<ProjectReference>entries. - Pre-build or post-build events that always write files. A pre-build step that stamps a version file with the current timestamp makes that file newer than every output on every build, forcing recompilation of anything that includes it. Fix: only write the file when its content would actually change (compare before overwriting), or move version stamping to CI builds only.
- Custom targets without Inputs/Outputs. A target that declares neither always runs, and if it writes files consumed by compilation, it poisons the chain. Always declare both attributes on custom targets that produce files.
- "Copy to Output Directory" on large content. Copying is cheap but still runs per build; with hundreds of files it adds up. This is usually acceptable, but know that it is timestamp-driven too — files are only copied when newer.
- Clock skew on network shares or containers. Timestamp comparison assumes a trustworthy clock. Building from a mounted volume with skewed time can cause spurious rebuilds or, worse, skipped rebuilds of genuinely changed files.
Up-to-date checks in Visual Studio vs. MSBuild
Inside the IDE, Visual Studio (for SDK-style projects) adds its own fast up-to-date check before even invoking MSBuild. It compares inputs and outputs using its own project model and logs the result to the Build output pane: look for FastUpToDate: project is up-to-date or a reason it is not. If the IDE check passes, MSBuild is never called at all, which is why pressing F5 with no changes is nearly instant. If the IDE check fails but MSBuild then skips everything anyway, the IDE's check is being more conservative than the build — usually harmless, but worth investigating if it happens constantly, because it means you pay for an MSBuild invocation on every run.
Limits and when to do a full rebuild
Incremental builds are a correctness bet on timestamps and declared dependencies. They do not protect you from:
- Undeclared inputs — a code generator reading a file MSBuild does not know about.
- Environment changes — a new compiler version or updated NuGet package can leave stale outputs that still look up-to-date in some edge cases.
- Corrupted intermediate state in
objfolders.
For release builds, CI pipelines, and any build whose artifacts you ship, run a clean build (msbuild /t:Rebuild or delete bin/obj first). The few extra minutes are cheap insurance against shipping a binary stitched together from mismatched intermediate state.
How to verify your fix
After correcting references or build events, run the verification from the research-backed workflow: build once, use touch (or edit and save) a single .cs file in one project, then build again with /v:d. Confirm that only that project and its transitive dependents recompile, and that unrelated projects log "up-to-date" skip messages. Time both builds with msbuild /v:m and compare wall-clock times — a healthy incremental build of a large solution after a single-file change should take seconds, not minutes. If a project still rebuilds unexpectedly, the detailed log will name the input file that was newer than the output, which points you directly at the offending step.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.