The State of .NET CI 2026
NuGet restore, the Windows-runner premium, and the build matrix that quietly doubles your bill, and the runner choices that take it all back.
Executive summary
The .NET CI story in 2026 is increasingly a Linux story. Modern .NET builds and tests run cleanly on Linux, the SDK is first-class there, and the per-minute economics strongly favor it. Yet a large share of real-world pipelines still pin to Windows runners out of habit, a legacy framework target, or a single Windows-only test that drags the whole job onto the expensive operating system. That choice is not free. A Windows CI minute costs twice a Linux minute on hosted runners, and that premium compounds across every leg of the build matrix rather than applying once.
Underneath the operating-system question sits NuGet. A cold restore of a large solution graph is one of the biggest single slices of the .NET minute, rivaling test execution on the first run of a branch. Like most package ecosystems it is highly cacheable, but only when the cache genuinely persists across runs instead of being rebuilt and re-uploaded per job. Stack a target-framework or build-configuration matrix on top of an uncached restore and the bill multiplies again, often for legs that were never going to surface a real regression on a routine push.
Because the two dominant costs, the Windows premium and the cold NuGet restore, are structural rather than test-logic problems, the runner layer decides the bill far more than the application code does. Right-sizing to Linux where the work is portable, keeping restore and build caches warm across the fleet, and automatically retrying transient restore and test failures are where a managed runner reclaims the .NET minute most. None of these require touching a single test or rewriting a project file.
This report quantifies where a .NET CI minute actually goes, how the Windows premium and the matrix multiplier interact, and which interventions return the most spend for the least effort. The throughline mirrors the broader State of CI/CD picture: most waste in .NET pipelines is mechanical, repeated, and cacheable, which means it is addressable with configuration and a better runner layer rather than a heroic refactor.
For engineering leaders, the practical message is that .NET CI cost is not destiny. The teams that move portable work to Linux, persist their caches, and trim the matrix to what each push actually needs run pipelines several times cheaper than peers on identical codebases, and they do it while holding change-failure rate in the elite band rather than firefighting flaky restores by hand.
Relative billed cost for an identical build+test pipeline across runner choices. · Source: GitHub Actions pricing + Latchkey analysis (modeled)
Published GitHub-hosted per-minute rates vs a managed alternative. · Source: GitHub Actions pricing + Latchkey rates
Email me the report
The full report is right here on this page, free. Want the link in your inbox to read later or share, plus new Latchkey reports as they drop? Drop your email and we will send it over.
Sent! Check your inbox for the report link.
No spam. Unsubscribe anytime.
The Windows premium is the most avoidable cost in .NET CI
A Windows CI minute costs twice a Linux minute on hosted runners, and that multiplier is not a rounding error in a .NET pipeline, it is frequently the dominant term in the bill. Modern .NET runs cleanly on Linux for the large majority of builds and tests, so the teams that default whole pipelines to windows-latest are usually paying double for work that would pass identically on the cheaper operating system.
The pattern is consistent when teams audit their .NET bills. The expensive Windows runner ends up doing cheap, portable work: restoring packages, compiling class libraries, running unit tests that never touch a Windows-specific API. The genuinely Windows-bound surface, things like WPF or WinForms UI tests, COM interop, or producing a signed Windows installer, is much smaller than the pipeline that grew up around it.
Moving the default build and test legs to Linux and reserving Windows only for the legs that actually require it typically halves the per-minute rate across most of the matrix with no loss of coverage. The chart below shows the same pipeline billed across runner choices, and the gap between the Linux leg and the Windows leg is the spend most teams can reclaim on day one.
- A Windows minute is 2x a Linux minute on hosted runners; a macOS minute is 10x.
- Restore, class-library compilation, and most unit tests are portable and belong on Linux.
- Reserve Windows for WPF/WinForms UI tests, COM interop, and signed Windows packaging.
NuGet restore is a third of a cold .NET minute
On a cold runner, restoring a large solution's NuGet graph can rival test execution as the single biggest slice of the minute. Our modeling puts it at roughly a third of a cold .NET pipeline minute. The restore is deterministic and highly cacheable, which is exactly why paying for it on every run is such avoidable waste: the package set rarely changes between pushes, yet a cold runner pulls it from scratch each time.
The trap is that hosted caches cold-miss on the first run of a branch and frequently re-upload the full package set per job in a matrix, so a team can have caching configured and still pay the cold restore repeatedly. A cache that survives only within a single workflow run, or that is keyed too loosely and serves stale content, gives the appearance of caching without the savings.
A persistent restore cache that genuinely survives across runs and across the fleet is the cheapest high-leverage win available to a .NET pipeline. Keyed correctly on packages.lock.json and the SDK version, it turns a cold multi-second restore into a near-instant warm hit. The donut below shows how large a slice restore occupies on a cold run, which is the slice a warm cache reclaims directly.
Modeled split of a billed minute for a typical cold .NET solution pipeline. · Source: Latchkey analysis (modeled)
Target-framework matrices multiply minutes faster than teams expect
Each added target framework and each added build configuration behaves close to a linear multiplier on billed minutes. A multi-target solution building net9.0 and net8.0 in both Debug and Release, with a legacy .NET Framework leg for compatibility, can approach four times the minutes of a single-target build, because every leg re-runs the full restore, build, and test cycle.
Most of those legs are redundant on a routine per-push check. A one-line change to a service is exceedingly unlikely to pass on net9.0 Release and fail only on net8.0 Debug, yet the default matrix pays for all four permutations on every commit. The wide matrix earns its keep at release time, when cross-framework and cross-configuration compatibility genuinely matters, not on every push.
The clean cut is to gate the full matrix to pre-release and tagged builds, and test a single representative target on each commit. This keeps the compatibility guarantee where it counts while removing the bulk of the per-push multiplier. The chart below shows how quickly the multiplier climbs as TFMs and configurations are stacked.
Relative billed minutes vs a single-target build as TFMs and configs are added. · Source: Latchkey analysis (modeled)
The Windows premium and the matrix compound each other
The two dominant .NET costs are multiplicative, not additive. A four-leg matrix run on Windows runners pays both the two-times operating-system premium and the roughly four-times matrix multiplier at the same time, which is how .NET teams end up with CI bills several times larger than an equivalent Linux pipeline would cost for identical coverage.
This compounding is what makes .NET CI bills feel disproportionate to the size of the codebase. The bill is not being driven by test volume or build complexity, it is being driven by two configuration choices that each multiply the other. A team can have a fast, well-written test suite and still pay a heavy premium purely from where and how widely it runs.
Untangling the two, Linux by default and the matrix trimmed to what each push needs, is usually the single biggest .NET cost reduction available, and it requires no change to application code. The savings are structural and immediate: the next invoice reflects them, because the wasted minutes simply stop being billed.
Transient restore and test flakes tax an otherwise clean pipeline
A meaningful slice of .NET CI minutes goes to re-runs from failures that were never really broken. NuGet restore is network-bound and depends on a remote feed, so a registry timeout or a transient feed error fails a build that would pass cleanly on retry. Integration tests that spin up a database or a message broker add their own mechanical flakiness from container start races and port contention.
These failures are not test bugs and they do not need an engineer. They are environmental hiccups: a feed blip, an out-of-memory kill on an undersized runner, a race in test setup. Quarantining or rewriting the tests is the wrong first response, because the test logic is fine and the environment is what hiccuped.
Self-healing runners attack this directly by retrying the failed step on a fresh environment when the failure matches a known-transient signal, before a human ever sees a red check. The minutes spent become recovery minutes rather than wasted ones, and the developer never loses focus to a spurious red build. This is what lets a .NET team hold change-failure rate in the elite 0-15% band without re-running jobs by hand.
Caching the build, not just restore, is the second-order win
Restore gets most of the caching attention, but the dotnet build and publish steps recompute work that is frequently identical to the previous run. Compiled assemblies for unchanged projects, intermediate obj output, and source-generator results are all candidates for incremental reuse rather than a clean rebuild on every push.
The .NET incremental build is capable here, but it only helps when the obj and bin output and the relevant caches persist across runs. On an ephemeral hosted runner that starts clean every time, the incremental build has nothing to build on, so it falls back to a full compile that the cache could have avoided.
Persisting the build output alongside the restore cache extends the warm-pipeline benefit from restore into compilation. For a large solution this compounds with the restore savings, moving the cost driver from the size of the solution back toward the size of the change, which is the same principle that breaks the cost curve in the broader CI picture.
Managed runners erase both the premium and the transient re-runs
Because the dominant .NET costs are the Windows premium and the cold restore, the runner layer decides the bill more than the code does. Managed runners run the common, portable legs at a materially lower effective cost than hosted Windows minutes, targeting roughly 69% below GitHub-hosted rates, which directly attacks the most avoidable line item.
On top of the per-minute saving, a managed layer keeps the restore and build caches warm across the fleet rather than per-run, so the cold-restore slice shrinks toward zero on warm pipelines. The two savings stack: a lower rate on fewer billed minutes, because the cache removes the minutes that the rate would otherwise apply to.
And because a managed runner provisions a fresh, isolated environment per job, it self-heals the transient restore timeouts and flaky integration tests automatically while delivering the clean-environment isolation that prevents state from bleeding between runs. The cost story, the reliability story, and the isolation story point at the same architecture, which is what makes the managed inflection durable for .NET rather than a passing optimization.
- Portable legs run roughly 69% below hosted Windows rates on managed runners.
- Warm fleet-wide caches shrink the cold-restore and cold-build slices on every run.
- Per-job isolation self-heals transient restore and integration-test flakes without manual re-runs.
Recommendations
Default the matrix to Linux, reserve Windows for what needs it
Audit which jobs genuinely require Windows and move everything else, restore, class-library compilation, and portable unit tests, to Linux. Keep Windows only for WPF and WinForms UI tests, COM interop, and signed Windows packaging. This single reshaping usually halves the per-minute rate across most of the matrix with no loss of coverage and is the fastest path to a lower .NET bill.
Persist a real NuGet restore cache across runs
Key the restore cache on packages.lock.json and the SDK version, and confirm it survives across runs and across matrix legs rather than re-uploading per job. Measure the cache hit rate the way you measure test coverage, because a cache nobody measures is a cache that silently cold-misses while you keep paying the full restore.
Gate the wide TFM matrix to pre-release
Test one representative target framework and configuration on each commit, and run the full multi-target, multi-config matrix only on pre-release and tagged builds. This preserves the cross-framework compatibility guarantee where it matters while removing the per-push multiplier that drives most of the matrix cost.
Persist build output, not just restore
Extend caching from NuGet restore into the dotnet build by persisting obj and bin output so the incremental build has something to build on. On a large solution this compounds with the restore saving and shifts the cost from the size of the solution toward the size of the change.
Auto-heal transient restore and integration flakes
NuGet feed timeouts and container-start races in integration tests are mechanical failures that pass on a clean retry. Retrying on a fresh environment removes the bulk of the flaky re-run tax without touching test code and keeps infrastructure-caused red checks from ever reaching a developer.
Outlook
Expect the Linux-first direction in .NET CI to harden through 2026. As the SDK and the broader .NET ecosystem continue to treat Linux as a first-class target, the remaining reasons to pin a pipeline to Windows keep shrinking to the genuinely platform-bound surface. The teams that have already moved their portable work to Linux will keep widening their cost advantage over peers still defaulting to windows-latest out of habit.
Caching is the other axis where the gap widens. As solutions grow, the difference between a cold pipeline that restores and rebuilds everything on every push and a warm pipeline that restores almost nothing becomes the difference between a bill that scales with the codebase and one that scales with the change. Persistent, fleet-wide caching is what keeps the .NET minute count flat as the solution grows.
For most .NET teams the practical takeaway is that the expensive parts of their CI, the Windows premium, the cold restore, and the redundant matrix, are all mechanical and all addressable without rewriting tests. A budget for pipeline latency and cost, a Linux-first default, persistent caches, and a runner layer that self-heals transient failures turn .NET CI from a disproportionate tax into an ordinary, well-controlled line item.
Methodology
This report focuses on the .NET CI pipeline: NuGet restore and caching, the Windows-runner cost premium, target-framework build matrices, and incremental build reuse. The Windows-versus-Linux per-minute multiple is from published GitHub Actions pricing; minute-split, matrix-multiplier, and relative-cost figures are Latchkey modeled estimates derived from typical .NET solution shapes; the CI adoption headline is from the Stack Overflow Developer Survey. Where a figure is labeled modeled it is an illustrative estimate intended to show direction and magnitude rather than a precise population value. Figures labeled "modeled" are illustrative estimates derived from public pricing and typical pipeline shapes, not a primary survey; figures attributed to a named source reflect that source. Pricing reflects published rates at time of writing and should be verified against current provider pricing.
Sources
- Stack Overflow Developer Survey
- JetBrains Developer Ecosystem Survey
- GitHub Actions - billing & pricing
- GitHub Actions documentation