The .NET Support Lifecycle and Choosing a Target
Objective
Every .NET version has an expiry date, and the date is part of the engineering
decision, not a footnote. Picking a target framework decides how long you get
security patches, when you are forced to upgrade, and which SDK and container
images your build depends on. This concept explains the support policy (LTS,
STS, active and maintenance phases), the difference between the SDK that builds
your code and the runtime that runs it, how global.json pins the SDK, how the
host picks a runtime at startup, and how to choose a target in late 2026.
Use Cases
- Deciding whether a new service should target
net10.0or stay on the version the rest of the company uses. - Pinning the SDK in a repository so every developer and the CI agent build with the same feature band.
- Explaining why an app compiled for
net8.0still starts on a machine that has only newer runtimes, or why it refuses to. - Choosing a container tag that receives patches automatically, or one that is reproducible byte for byte.
- Planning the migration of
net8.0andnet9.0services before their support ends.
Deep Dive
LTS, STS and the two support phases
A new major version ships every November. Releases alternate between two tracks, and the only difference between them is how long Microsoft patches them, not their quality:
- LTS (Long Term Support): three years of free support and patches.
- STS (Standard Term Support): two years (24 months).
Each release goes through an active phase, with functional and security fixes, and then a maintenance phase, which is the final 6 months and receives security fixes only. After that comes end of support: the app keeps running, but no more fixes arrive.
Support also has a condition: you must be on the latest patch. Patches are
cumulative, ship roughly monthly (security ones on Patch Tuesday, the second
Tuesday of the month), and a machine on 10.0.0 while 10.0.13 exists is not
in a supported state.
State of the official support policy page in October 2026:
| Version | Type | Released | Phase | End of support |
|---|---|---|---|---|
| .NET 10 | LTS | November 11, 2025 | Active | November 14, 2028 |
| .NET 9 | STS | November 12, 2024 | Maintenance | November 10, 2026 |
| .NET 8 | LTS | November 14, 2023 | Maintenance | November 10, 2026 |
Notice that .NET 8 (LTS) and .NET 9 (STS) end on the same day. The 24 month STS window makes the STS release expire together with the LTS before it, so choosing an STS no longer means a much shorter life than the previous LTS. Dates move when the policy changes, so read the policy page before a plan, not this table.
SDK versus runtime
The runtime (Microsoft.NETCore.App, plus shared frameworks such as
Microsoft.AspNetCore.App) runs applications. The SDK contains the
compilers, MSBuild, NuGet and the dotnet CLI commands, and it ships with a
runtime. They have different version schemes. The runtime uses
major.minor.patch. The SDK uses major.minor.Fnn, where F is the
feature band (100, 200, 300...) and nn the patch inside the band. A new
band can bring a newer MSBuild or NuGet; a patch inside the band cannot.
Both lists come from the CLI. Output from this machine:
plaintext$ dotnet --list-sdks 9.0.307 [/usr/local/share/dotnet/sdk] 10.0.100 [/usr/local/share/dotnet/sdk] $ dotnet --list-runtimes Microsoft.AspNetCore.App 9.0.11 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 10.0.0 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.NETCore.App 8.0.22 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App] Microsoft.NETCore.App 9.0.11 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App] Microsoft.NETCore.App 10.0.0 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
dotnet sdk check compares that list with the published releases and tells you
what is stale:
text.NET SDKs: 9.0.307 .NET 9.0 is going out of support soon. 10.0.100 Patch 10.0.113 is available.
An SDK can build every older target framework it knows, so the newest SDK
builds net8.0 projects too. Targeting a framework that is already out of
support still compiles, with a warning:
textwarning NETSDK1138: The target framework 'net7.0' is out of support and will not receive security updates in the future.
Pinning the SDK with global.json
Without global.json, every dotnet command uses the highest SDK installed. To
make builds reproducible, put a global.json at the repository root. The CLI
searches for it from the current directory upward, so subfolders inherit it.
plaintext{ "sdk": { "version": "9.0.300", "rollForward": "latestPatch" } }
rollForward defines the acceptable range. These results were produced with
SDKs 9.0.307 and 10.0.100 installed:
version |
rollForward |
dotnet --version |
|---|---|---|
| (no global.json) | 10.0.100 |
|
9.0.300 |
(none, default is patch) |
9.0.307 |
9.0.300 |
latestPatch |
9.0.307 |
9.0.100 |
latestFeature |
9.0.307 |
9.0.400 |
latestFeature |
fails, no 9.0.4xx or higher installed |
8.0.100 |
latestPatch |
fails, no 8.0.1xx installed |
8.0.100 |
latestMajor |
10.0.100 |
A failure is loud and stops every SDK command with exit code 155. The .NET 10
SDK added an errorMessage property so the failure can say how to fix it:
plaintext{ "sdk": { "version": "9.0.400", "rollForward": "latestFeature", "errorMessage": "This repo needs a .NET 9 SDK from the 9.0.400 band or later. Run ./eng/install-sdk.sh." } }
text* You intended to execute a .NET SDK command: This repo needs a .NET 9 SDK from the 9.0.400 band or later. Run ./eng/install-sdk.sh.
latestFeature is a sensible default for teams: it guarantees a minimum band
and still accepts patches and newer bands of the same major version.
How the host picks a runtime
global.json only chooses the SDK. At startup, the host reads the target in
*.runtimeconfig.json and applies a different roll-forward policy. The default
is Minor: use the latest patch of the requested major.minor, or a higher
minor if that one is missing, but never another major. Measured here with a
file-based app built for net8.0:
csharp#:property TargetFramework=net8.0
using System.Runtime.InteropServices;
Console.WriteLine("Built for: net8.0");
Console.WriteLine($"Running on: {RuntimeInformation.FrameworkDescription}");
// dotnet run which-runtime.cs -> Running on: .NET 8.0.22
// DOTNET_ROLL_FORWARD=LatestMajor dotnet run which-runtime.cs -> Running on: .NET 10.0.0The same program built for net7.0 compiles (with NETSDK1138) but refuses to
start on this machine, because no 7.x runtime is installed and the default
policy does not cross majors:
textYou must install or update .NET to run this application. Framework: 'Microsoft.NETCore.App', version '7.0.0' (arm64) The following frameworks were found: 8.0.22 at [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]
The policy can be set in the project (<RollForward>), in
runtimeconfig.json, with DOTNET_ROLL_FORWARD or with --roll-forward, the
last one winning. Crossing a major is a deliberate decision, because major
versions may contain breaking changes.
Container images follow the same versions
The official images live at mcr.microsoft.com/dotnet/{sdk,aspnet,runtime,runtime-deps}.
The tag encodes the version and the variant: aspnet:10.0 floats to the newest
10.0 patch, aspnet:10.0.13 is fixed at one patch, and suffixes select the
distribution or a smaller variant (10.0-noble, 10.0-noble-chiseled,
10.0-alpine). Build with sdk:10.0 and run with aspnet:10.0 or
runtime:10.0. A floating tag picks up security patches on rebuild; a fixed
tag or a digest is reproducible but only gets patches when someone bumps it.
What to target in late 2026
- New services and apps:
net10.0. It is the only version in active support and stays supported until November 2028. - Existing
net8.0andnet9.0code: both leave support on November 10,- Move to
net10.0now; staying means running without security fixes.
- Move to
- Libraries: target the lowest version your consumers still run, or multi-target, and build them with the newest SDK.
Trade-offs
- LTS gives a long window but ages the platform. You skip a year of runtime and library improvements to upgrade every two years instead of every year.
- STS no longer costs much support time, but it forces a yearly rhythm. With 24 months, .NET 9 ended together with .NET 8; whoever adopted 9 still had to move to 10 on the same date.
global.jsonwithdisableorlatestPatchmakes builds reproducible and fragile. A new teammate with a different band gets exit code 155 before anything compiles.plaintext{ "sdk": { "version": "10.0.100", "rollForward": "disable" } }- Forcing
LatestMajorkeeps an old app running on a new runtime, without testing. It hides the need to retarget and can expose behavior changes of the new major in production. - Floating image tags patch automatically but change under you. A rebuild on Monday can run a different patch than Friday's. Pinning a digest fixes that and moves the patching work to your pipeline (Dependabot or Renovate).