RE:NODE

App hosting11 min read

.NET versions, LTS vs STS and when to upgrade

Which .NET version to run in production: LTS and STS support dates, what end of support means, upgrading target frameworks, global.json and roll-forward.

0 readers

Run an LTS release in production unless you have a reason not to, and plan the upgrade before support ends rather than after. As of October 2026 that means .NET 10, the current long-term support release, supported until November 2028. .NET 8 (LTS) and .NET 9 (STS) both reach end of support on 10 November 2026 - a few weeks from now - so anything still on either of them should be moving. .NET 11 is due in November 2026 as a standard-term release. This post explains the support model, what actually happens when a version goes out of support, how to move a project between versions without surprises, and what global.json and roll-forward settings do to which SDK and runtime you get.

The release model: one major version a year#

Microsoft ships a new major version of .NET every November. Even-numbered releases are LTS (long-term support) and odd-numbered releases are STS (standard-term support).

TypeSupport lengthExamples
LTS3 years.NET 6, .NET 8, .NET 10
STS2 years (18 months up to .NET 7).NET 7, .NET 9, .NET 11

The STS window used to be 18 months. Starting with .NET 9, Microsoft extended it to 24 months, which is why .NET 9 now ends on the same day as .NET 8 instead of six months earlier. An STS release is not a beta - it is fully supported, receives the same monthly patches, and is used in production by plenty of teams who want features a year earlier. The trade is that you upgrade every year instead of every two or three.

The current lifecycle, for versions you are likely to meet:

VersionTypeReleasedEnd of support
.NET 6LTSNovember 2021November 2024 (ended)
.NET 7STSNovember 2022May 2024 (ended)
.NET 8LTSNovember 202310 November 2026
.NET 9STSNovember 202410 November 2026
.NET 10LTSNovember 2025November 2028
.NET 11STSexpected November 2026expected November 2028

Patches ship on Patch Tuesday, the second Tuesday of each month. A support date is the last patch day; after it, the version gets no security fixes at all. Microsoft's official .NET support policy page is the source to check before you plan around any date in this table.

What end of support actually means#

Nothing switches off on the end-of-support date. An app on .NET 8 will start on 11 November 2026 exactly as it did the day before. What changes:

  • No more security patches. The next vulnerability in Kestrel, the TLS stack, System.Text.Json or the runtime itself stays open in your version forever. For an internet-facing web app, that is the whole argument.
  • Container images stop being updated. The mcr.microsoft.com/dotnet/aspnet:8.0 tags stay pullable, but the base operating system packages underneath them are no longer refreshed either.
  • Libraries move on. NuGet packages drop old targets over the following year or two, and the version of a library that fixes a bug you hit may not support your framework.
  • Hosts and tooling move on. Build agents, SDK installers and hosting images eventually stop carrying old runtimes.

The practical rule: an LTS release gives you a year of overlap with the next LTS. Upgrade during that year, when the new version has had its first few patches and the libraries you use have caught up, rather than in the final month.

Target framework, SDK and runtime: three different versions#

Most upgrade confusion comes from treating these as one number.

  • Target framework - what your project compiles against, set in the project file as <TargetFramework>net10.0</TargetFramework>. It decides which APIs you can call and which runtime the app needs.
  • SDK - the toolchain that builds the project: dotnet build, dotnet publish, the compilers. SDK versions look like 10.0.100; the hundreds digit is the feature band (10.0.1xx, 10.0.2xx) and the last two digits are the patch.
  • Runtime - what runs the compiled app. ASP.NET Core apps need the ASP.NET Core shared runtime of the same major version, versioned like 10.0.0, 10.0.1 and so on.

An SDK can build for its own version and every earlier target framework, so the .NET 10 SDK builds a net8.0 project without complaint. The reverse does not work: the .NET 8 SDK cannot build net10.0. A runtime runs apps targeting its major version, and with roll-forward settings, sometimes later ones.

bash
$ dotnet --list-sdks8.0.415 [/usr/share/dotnet/sdk]10.0.100 [/usr/share/dotnet/sdk]$ dotnet --list-runtimesMicrosoft.AspNetCore.App 10.0.0 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]Microsoft.NETCore.App 10.0.0 [/usr/share/dotnet/shared/Microsoft.NETCore.App]

The error people meet when the runtime and target do not match is unmistakable:

code
You must install or update .NET to run this application.Framework: 'Microsoft.AspNetCore.App', version '10.0.0' (x64)The following frameworks were found:  8.0.11 at [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]

On a hosted app server, the runtime available is whatever the server's image provides, so find out which SDK and runtime it carries before you retarget. A self-contained publish sidesteps this entirely by shipping the runtime inside your build output, at the cost of a much larger deployment; dotnet publish and runtime options covers that trade.

Pinning the SDK with global.json#

Without a global.json, dotnet uses the newest SDK installed on the machine. That is usually what you want on a laptop and occasionally a problem on a build server, where an SDK update can change analyser warnings, default behaviours or the format of generated files between two builds of the same commit.

global.json
{  "sdk": {    "version": "10.0.100",    "rollForward": "latestFeature"  }}

version is the minimum SDK. rollForward decides how far dotnet may go above it:

rollForwardAllows
patchSame feature band, newer patch only
latestPatchHighest patch in the same feature band (default when version is set)
feature / latestFeatureNewer feature bands within the same major.minor
minor / latestMinorNewer minor versions
major / latestMajorAny newer SDK
disableExactly the version listed

latestFeature is a sensible choice for most repositories: builds stay on .NET 10 SDKs and still pick up patches and feature bands. disable sounds safe and breaks the build on every machine that has a patch newer than yours. If no installed SDK satisfies the file, the error names the version it wanted - install that SDK or relax the policy.

global.json affects the SDK only. It does not change the target framework, and it does not change which runtime the app runs on.

Runtime roll-forward#

A framework-dependent app targeting net10.0 runs on the highest installed 10.0.x patch of the runtime, automatically. This is how a patched runtime gets your app its security fixes without a rebuild, and it is why you should not try to pin a specific runtime patch.

Across minor and major versions, the behaviour is controlled by the RollForward project property (or the DOTNET_ROLL_FORWARD environment variable):

RollForwardBehaviour
Minor (default)Roll to a higher minor version if the requested one is missing
LatestPatchOnly patches of the requested major.minor
MajorRoll to a higher major version if no matching one exists
LatestMinor / LatestMajorAlways use the highest available
DisableExact version only

Major lets a net8.0 app start on a machine that only has the .NET 10 runtime. It usually works, and it is not a substitute for upgrading: you are running on a runtime the app was never tested against, with any breaking changes between the two versions applied silently. Use it to buy time in an emergency, not as a policy.

Upgrading a project, step by step#

Moving from .NET 8 to .NET 10 is, for most web apps, a short job if done in order:

  1. Install the new SDK and add or update global.json so the repository builds with it.
  2. Change the target framework in every project: net8.0 to net10.0. In a solution with many projects, a Directory.Build.props that sets TargetFramework once saves editing each file.
  3. Update the Microsoft packages to the matching major version. Packages such as Microsoft.AspNetCore.Authentication.JwtBearer, Microsoft.EntityFrameworkCore.* and Microsoft.Extensions.* are versioned with the runtime; a net10.0 app should use their 10.x versions.
  4. Update third-party packages, especially database providers. Npgsql, the Pomelo MySQL provider and others release a version per EF Core major; check that the one you need exists before you start.
  5. Build with warnings visible, and read them. New analysers and obsoletions appear as warnings first.
  6. Read the breaking changes list for each version you skip. Microsoft publishes one per release in the .NET documentation, sorted by area. Going from 8 to 10 means reading both the 9 and the 10 lists.
  7. Run the tests, then run the app against a copy of production data, and deploy to a test server before production.
bash
$ dotnet list package --outdated$ dotnet list package --vulnerable --include-transitive

The second command is worth running on every project regardless of upgrades: it lists packages with known vulnerabilities, including ones pulled in indirectly.

Some changes from recent versions that do catch people:

  • .NET 8 changed the default port in Microsoft's ASP.NET Core container images from 80 to 8080, and added ASPNETCORE_HTTP_PORTS as a simpler way to set it. Apps that relied on port 80 inside the container stopped answering after a base image bump.
  • .NET 9 removed `BinaryFormatter`'s implementation - it throws at runtime. Old caching or session code that serialised objects with it fails only when that code path runs.
  • EF Core majors follow the runtime. EF Core 10 requires .NET 10, so you cannot take the new EF Core without the new runtime, and migrations generated by a newer EF Core tool should be reviewed before applying. EF Core migrations in production covers how to apply them safely.

Rolling an upgrade out without betting production on it#

The safest upgrade is the one you can undo in a minute. Keep the upgrade on its own branch, and deploy that branch to a second, small server before it goes anywhere near the one your users hit. A test server that pulls the upgrade branch on start, pointed at a copy of the production database, catches the two failures unit tests miss: a dependency that behaves differently at runtime, and a migration that is slow or locks a table on real data. Running a test server beside production covers how to keep the two apart, and deploying an ASP.NET Core app from GitHub covers the deploy itself.

Before the production cutover, take a backup of the database and make sure the previous build is one commit away. If the new version misbehaves, reverting the target framework commit and redeploying is the rollback; a database migration that has already run is the part that cannot be reverted that easily, which is why schema changes should go out in a separate deploy before or after the runtime upgrade, not in the same one.

Upgrading a net8.0 library to multi-target is a good middle step when several apps depend on it: <TargetFrameworks>net8.0;net10.0</TargetFrameworks> (note the plural) builds both, and each app picks up the matching one.

LTS or STS: which should you run?#

LTS if the app is maintained in bursts, if upgrades need sign-off, or if the team is one person who has other things to do. Three years of patches and a year of overlap with the next LTS fits a slower rhythm.

STS if you deploy continuously, upgrade dependencies routinely, and want features - performance improvements in particular land every year - twelve months earlier. With 24 months of support, an STS release is no longer the short-fused choice it used to be, but it still ends at the same time as or before the previous LTS, so you are committing to an upgrade every year.

What you should not run is a version that is out of support, or a preview in production. Previews and release candidates appear from February each year; the release candidates carry a go-live licence from Microsoft, which means support for production use, but an app that depends on a preview runtime on a hosted server depends on that server carrying it.

C# language versions follow the SDK: C# 12 shipped with .NET 8, C# 13 with .NET 9, C# 14 with .NET 10. The default LangVersion is the one that matches your target framework, and setting it higher than the target supports is unsupported even when it compiles.

.NET Framework is a different product#

A last point that matters for hosting: .NET Framework 4.x is not .NET. It is the original Windows-only framework, still supported as part of Windows, still running a great deal of software, and it does not run on Linux. An ASP.NET (not ASP.NET Core) app on .NET Framework 4.8 cannot be deployed to a Linux container at all. Moving it means porting to ASP.NET Core on .NET 10, which for MVC apps is a real migration project rather than a retarget - System.Web does not exist in modern .NET.

Libraries that need to serve both worlds target netstandard2.0, which .NET Framework 4.6.1 and later and every modern .NET can consume. New libraries for modern .NET only should target net8.0 or later directly.

FAQ#

Is .NET 8 still safe to use?

Until 10 November 2026 it receives security patches, so it is safe today. After that date it receives nothing. If you are on .NET 8 now, the upgrade to .NET 10 should be scheduled, not considered.

Should I skip .NET 11 and wait for .NET 12?

If you run LTS releases, yes: go from 10 to 12, which is expected in November 2027, during the year when both are supported. If you run STS releases, take 11 when it ships and its first patch is out.

Do I need to rebuild my app for every monthly patch?

No, for framework-dependent apps. The app rolls forward to the newest installed patch of its runtime automatically. Self-contained apps carry their own runtime, so they do need a rebuild and redeploy to get each patch.

Can a .NET 8 app run on the .NET 10 runtime?

Only with RollForward set to Major or LatestMajor, and then without any testing against the newer runtime's breaking changes. Retargeting to net10.0 and testing is the proper fix.

What does global.json do if I do not have one?

Nothing, which means the newest installed SDK builds the project. That is fine locally and can cause drift on build servers. Add one with rollForward set to latestFeature to keep builds on the major version you intend.


Comments

Completely anonymous: no account, no email, no cookie. We store the name you type, the text and the time - nothing else. Links are limited and markup is not rendered.

0/2000