.NET 11 vs .NET 10: We Benchmarked Both on a Real Production App (Should You Upgrade?)
.NET 11 vs .NET 10 benchmarked on Mattrx in production — the real throughput, latency, and memory wins, the new features, and whether to upgrade.
- Author
- Randhir Jassal
- Published
- Reading time
- 20 min read
- Views
- 2 views
We run Mattrx on .NET 10 (the current LTS) in production, and we put .NET 11 previews through the same benchmark harness on the same workload. This is what actually changed — runtime, GC, ASP.NET Core, C#, AOT, and the AI tooling — with real numbers, before/after code, and a straight answer on whether to upgrade. Heads-up: .NET 10 figures are GA production numbers; .NET 11 figures are from preview builds (GA is November 2026) and will shift before release.
TL;DR
Version-chasing is a bad habit; measuring is a good one. We benchmarked our actual production workload — not a synthetic ProtoBench — across .NET 9 (where we were), .NET 10 (where we are), and .NET 11 previews (where we're headed). The honest summary: .NET 10 is a no-brainer LTS upgrade with real, free performance; .NET 11 looks like another solid step but it's a preview, so plan for it, don't deploy it.
| Dimension | .NET 10 (GA, LTS) | .NET 11 (preview — GA Nov 2026) |
|---|---|---|
| Status | Production-ready LTS (3-yr support) | Preview / STS — evaluate, don't ship |
| Runtime / JIT | Mature codegen, AVX10.2, better PGO | Further codegen + vectorization (early) |
| GC | DATAS tuned + region GC mature | Incremental refinements (early) |
| ASP.NET Core | Built-in OpenAPI, minimal-API validation | Continued perf + minimal-API polish |
| Language | C# 14 (field, extension members) | C# 15 direction (preview) |
| Native AOT | Smaller, faster startup | Smaller still (early) |
| AI tooling | Microsoft.Extensions.AI stable | Deeper first-class AI integration (preview) |
| Our verdict | Upgrade now | Pilot in CI, ship after GA |
Production / benchmark numbers (Mattrx workload, same hardware, same load profile):
- Throughput per instance, .NET 9 → .NET 10: +11% (GA, measured in production).
- API p95, .NET 9 → .NET 10: 132 ms → 120 ms (−9%, free on upgrade).
- Working set per instance, .NET 9 → .NET 10: 415 MB → 380 MB (−8%).
- Startup (cold) on Native AOT, .NET 9 → .NET 10: 84 ms → 61 ms.
- Container image (AOT, trimmed), .NET 9 → .NET 10: 41 MB → 33 MB.
- .NET 10 → .NET 11 preview (early, not GA): throughput +6–9%, p95 −5–7%, startup −10% — directional, expect change.
- Migration effort 9 → 10: ~1.5 days for ~95k LOC, near-zero code changes.
- Lines of code C# 14 deleted (boilerplate): ~700 (
field+ extension members).
The one rule we operate by: upgrade to the LTS for the free wins; treat the next STS/preview as a pilot, not a deploy. The numbers reward upgrading — they don't reward chasing the bleeding edge into production.
The one mental shift
Developers argue about .NET versions like sports teams. The senior framing is colder: a runtime upgrade is a risk-vs-reward decision with measurable inputs, and the cadence matters as much as the features.
LTS is for production; STS/preview is for piloting. .NET ships every November — even-ish years are LTS (3-year support: .NET 8, 10), the others are STS (18 months: .NET 9), and the next version is in preview for most of the year before GA. Run the LTS in prod, benchmark the preview in CI, and let your numbers — not a release blog — decide when to move.
So this post isn't "11 is better than 10." It's: here's what 10 gave us (banked), here's what 11 previews suggest (pending), and here's how to decide for your workload.
The running example: Mattrx
Mattrx is a multi-tenant marketing-analytics SaaS — 110k MAU, Angular 19 front end, ASP.NET Core back end, Azure SQL, ~3,200 req/sec peak across six instances, ~95k LOC C#. We migrated 9 → 10 in production this cycle and benchmark 11 previews nightly in CI against a captured production traffic profile. Every "GA" number below is from production; every ".NET 11" number is from preview builds on that same harness.
.NET RELEASE CADENCE (why the upgrade decision is really a cadence decision)
.NET 8 ──LTS (3yr)──────────────────────►
.NET 9 ──STS (18mo)──────►
.NET 10 ──LTS (3yr)─────────────────────────────► ◄── Mattrx prod is HERE
.NET 11 preview ───────────────► GA (Nov 2026) ◄── pilot in CI, ship after GA
▲ you are reading this here (mid-2026)
Section 1 — Runtime & JIT: the free performance
The single best argument for upgrading is that the JIT gets smarter and your code gets faster without you touching it. .NET 10 brought more aggressive devirtualization, stack allocation of small fixed arrays, better loop optimizations, and broader AVX10.2 vectorization — all of which our hot paths (event aggregation, JSON serialization, the prediction loop) benefit from for free.
Before / After — same code, faster output
// SAME SOURCE on both runtimes — the difference is purely codegen.
// Hot path: summing event weights across a campaign window.
public double WeightedScore(ReadOnlySpan<float> weights, ReadOnlySpan<float> values)
{
double sum = 0;
for (int i = 0; i < weights.Length; i++) // .NET 10 auto-vectorizes this loop further;
sum += weights[i] * values[i]; // .NET 11 preview widens it again (AVX10.2)
return sum;
}
// where you DO opt in: TensorPrimitives uses the best available SIMD per runtime
public double WeightedScoreFast(ReadOnlySpan<float> weights, ReadOnlySpan<float> values)
=> TensorPrimitives.Dot(weights, values); // gets faster on each runtime, no rewrite
# diagnostic: benchmark YOUR hot path across runtimes — never trust a generic benchmark
dotnet run -c Release --framework net10.0 # BenchmarkDotNet harness
dotnet run -c Release --framework net11.0 # same harness, preview SDK
# compare allocations + ns/op; only ship what your numbers justify
HOT-PATH THROUGHPUT (Mattrx event aggregation, relative)
.NET 9 ████████████████████ 100% (baseline)
.NET 10 ██████████████████████ 111% (GA — banked)
.NET 11 ███████████████████████▌ ~118% (preview — directional)
Mattrx metric: the 9 → 10 jump gave +11% throughput on our aggregation path with zero code changes — pure JIT/codegen. Early .NET 11 previews show another ~6–9% on the same harness, but that's preview noise until GA.
Section 2 — GC & memory
Garbage collection improvements are the quiet win: lower working set means more headroom per instance, which means fewer instances. .NET 10's DATAS (Dynamic Adaptation To Application Sizes) is tuned to right-size the heap to the actual workload instead of over-committing, and region-based GC is mature.
Before / After — config, not code
<!-- .NET 10: Server GC + DATAS adapts heap to real load (good default for most web apps) -->
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
<!-- DATAS is on by default in .NET 8+; .NET 10 tuning makes it size tighter -->
</PropertyGroup>
# diagnostic: watch working set + Gen2 pause across runtimes under identical load
dotnet-counters monitor -p <pid> System.Runtime | grep -E "Working Set|Gen 2|Time in GC"
Mattrx metric: working set per instance dropped 415 MB → 380 MB on 9 → 10 — an 8% reduction that, combined with the throughput gain, is part of why our web tier holds peak on fewer instances. .NET 11 previews trim it a few more MB; not enough to bank on yet.
Section 3 — ASP.NET Core: built-in OpenAPI and faster minimal APIs
.NET 10's ASP.NET Core continued shrinking the gap between "quick to write" and "fast in production." Two concrete wins we used: built-in OpenAPI document generation (no third-party Swagger dependency) and validation in minimal APIs without a controller.
Before — third-party Swagger + manual validation
// BEFORE (.NET 8-era) — Swashbuckle dependency + hand-rolled validation in the handler
builder.Services.AddSwaggerGen(); // external package to maintain
app.MapPost("/api/campaigns", (CreateCampaign cmd) =>
{
if (string.IsNullOrWhiteSpace(cmd.Name)) return Results.BadRequest("Name required"); // manual
// ...
});
After — built-in OpenAPI + declarative validation
// AFTER (.NET 10) — OpenAPI in the box; DataAnnotations validated by the framework
builder.Services.AddOpenApi(); // built-in, no third-party dependency
app.MapOpenApi(); // serves the spec
app.MapPost("/api/campaigns", (CreateCampaign cmd) => Results.Created(...))
.WithName("CreateCampaign"); // validation runs from the record's attributes
public record CreateCampaign(
[Required, StringLength(120)] string Name, // validated by the framework, not by hand
[Range(0, 1_000_000)] decimal Budget);
Mattrx metric: dropping Swashbuckle for built-in OpenAPI removed a dependency (and its CVE surface), and moving validation to attributes deleted ~120 lines of repeated if (invalid) return BadRequest across minimal-API endpoints. Request overhead on validated endpoints dropped a hair (part of the overall p95 → 120 ms).
Section 4 — C# language: C# 14 deletes boilerplate
Each runtime ships a language version, and .NET 10 brought C# 14 — the most boilerplate-deleting release in years. Two features earned their keep immediately: the field keyword and extension members.
The field keyword — properties with logic, no backing field
// BEFORE (C# 13) — a manual backing field just to trim/guard a setter
private string _name = "";
public string Name
{
get => _name;
set => _name = value?.Trim() ?? ""; // boilerplate field declared above
}
// AFTER (C# 14) — `field` is the compiler-generated backing field; no manual declaration
public string Name
{
get;
set => field = value?.Trim() ?? ""; // `field` refers to the auto backing store
}
Extension members — extension properties and static members, not just methods
// AFTER (C# 14) — extend a type with a PROPERTY (not possible before), grouped in a block
public static class CampaignExtensions
{
extension(Campaign c) // C# 14 extension block
{
public bool IsLive => c.Status == CampaignStatus.Published && c.Spent < c.Budget;
public decimal Remaining => c.Budget - c.Spent; // an extension PROPERTY
}
}
// usage reads like a real member: if (campaign.IsLive) { ... }
Mattrx metric: field plus extension members let us delete roughly 700 lines of backing fields and helper-method ceremony across the domain layer, and the code reads closer to the domain. (.NET 11 will ship C# 15 with more of this; in preview, not final.)
Section 5 — Native AOT: startup and container size
For our worker services (the Reports PDF generator, the event ingest workers) Native AOT matters: faster cold start for scale-to-zero, and a smaller container. .NET 10 made AOT smaller and broadened library support; .NET 11 previews continue the trend.
<!-- worker .csproj — Native AOT for fast-start, small-image workers -->
<PropertyGroup>
<PublishAot>true</PublishAot>
<StripSymbols>true</StripSymbols>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
COLD START + IMAGE SIZE (Mattrx ingest worker, Native AOT)
startup (cold) container image
.NET 9 84 ms 41 MB
.NET 10 61 ms ◄── GA 33 MB ◄── GA
.NET 11 preview ~55 ms (early) ~30 MB (early)
Mattrx metric: the AOT ingest worker's cold start dropped 84 ms → 61 ms and image 41 MB → 33 MB on 9 → 10 — meaningful for a pool that scales 0→40 at month-end, because faster cold start means the burst is absorbed sooner.
Section 6 — AI tooling becomes first-class
The trajectory across these releases is that AI is becoming a built-in concern. Microsoft.Extensions.AI (the IChatClient / embeddings abstraction) stabilized so you can swap providers behind one interface — and .NET 11 previews push it deeper into the platform.
// .NET 10 — Microsoft.Extensions.AI: provider-agnostic chat behind one interface
builder.Services.AddChatClient(sp =>
new AzureOpenAIClient(endpoint, cred).AsChatClient("gpt-4o"));
public sealed class HelpService(IChatClient chat) // swap Azure OpenAI / others, no rewrite
{
public Task<ChatResponse> AskAsync(string q, CancellationToken ct) =>
chat.GetResponseAsync(q, cancellationToken: ct);
}
Mattrx metric: moving Mattrx Help onto the IChatClient abstraction meant our RAG layer no longer hard-depends on one SDK — we benchmarked two providers behind the same interface in an afternoon. The classical-ML side stays on ML.NET (no LLM needed); the platform now treats both as first-class.
Section 7 — The migration itself (what 9 → 10 actually took)
Talking about perf is easy; the upgrade is where teams hesitate. It's mostly mechanical, but "mechanical" still has steps worth getting right. Here is the exact PR shape we used.
// 1. global.json — pin the SDK so CI and every dev box agree on the version
{ "sdk": { "version": "10.0.100", "rollForward": "latestMinor" } }
<!-- 2. bump the target framework across all projects (Directory.Build.props is ideal) -->
<TargetFramework>net10.0</TargetFramework> <!-- was net9.0 -->
<LangVersion>14.0</LangVersion> <!-- opt into C# 14 (or leave the default) -->
# 3. update packages to matching majors (EF Core, ASP.NET Core, analyzers, etc.)
dotnet list package --outdated # or edit Directory.Packages.props (central versions)
# 4. build to SURFACE the new analyzer warnings — read them, don't blanket-suppress
dotnet build
# 5. run the full suite + the benchmark harness BEFORE you merge the upgrade
dotnet test && dotnet run -c Release --project benchmarks
The friction points we actually hit (budget for these — the upgrade is small, not zero):
- A handful of new analyzer warnings (nullability, newly-obsolete APIs). Fix or suppress each deliberately; a blanket
<NoWarn>hides real ones. - One transitive package lagged the .NET 10 majors by about a week — we pinned it until it shipped a compatible build.
- The trimming analyzer flagged an AOT-incompatible reflection call in a worker — a genuine latent bug the upgrade surfaced rather than caused.
Mattrx metric: the whole migration was a single PR, ~1.5 engineer-days for ~95k LOC, and it banked +11% throughput the day it merged — the best ROI change of the quarter, and the reason "stay current with the LTS" is a habit worth having.
Aggregate metrics
| Metric | .NET 9 | .NET 10 (GA) | .NET 11 (preview) |
|---|---|---|---|
| Throughput / instance | baseline | +11% | +6–9% (early) |
| API p95 | 132 ms | 120 ms | −5–7% (early) |
| Working set / instance | 415 MB | 380 MB | a few MB less |
| AOT cold start | 84 ms | 61 ms | ~55 ms (early) |
| AOT image size | 41 MB | 33 MB | ~30 MB (early) |
| Language | C# 13 | C# 14 | C# 15 (preview) |
| OpenAPI | third-party | built-in | built-in |
| Support model | STS (18mo) | LTS (3yr) | STS (18mo) |
| Production verdict | upgrade off it | run it | pilot only |
The pattern: .NET 10 banked real, free wins for the cost of ~1.5 days of migration. .NET 11 looks like more of the same — which is exactly why you pilot it now and ship it after GA, not before.
Should you upgrade? A decision checklist
- On .NET 8 or 9 → move to .NET 10 now. It's LTS, the migration is small, and the perf is free. There's no good reason to stay on an STS once the next LTS is out.
- On .NET 10 → stay. You're on the current LTS; you're done until you have a reason.
- Eyeing .NET 11 → pilot it in CI against a captured production traffic profile; don't deploy a preview.
- Benchmark your hot paths with BenchmarkDotNet across target frameworks — generic benchmarks don't reflect your workload.
- Check breaking changes and analyzer warnings on the new SDK before committing the upgrade PR.
- Pin the SDK via
global.jsonso CI and dev machines agree on the version. - Adopt language features (
field, extension members) opportunistically — they're free cleanup, not a migration blocker. - Wait for GA + at least one patch before putting a new major in production. Let
.0bugs surface on someone else's preview.
Honest stuff — the caveats that matter
-
.NET 11 is not released. As of this writing it's in preview; GA is November 2026. Every .NET 11 number here is from preview builds and will change. Do not make production decisions on preview benchmarks — pilot, then re-measure at GA.
-
Version perf gains are real but modest. A single-digit-to-low-double-digit throughput bump per release is excellent for free, but it won't fix an architectural problem. If your p95 is 2 seconds because of an N+1 query, no runtime upgrade saves you — fix the query first.
-
LTS vs STS is a support decision, not just a feature one. STS releases (like .NET 9) get 18 months of support; LTS (10) gets three years. For most enterprises, riding LTS-to-LTS (8 → 10 → 12) is the calmer cadence; jump onto STS only for a feature you specifically need.
-
Benchmark your workload, not a blog's. Our +11% is our aggregation/serialization mix. Yours will differ — could be more (vectorizable numeric code) or less (I/O-bound code where the CPU win is invisible). The only number that matters is the one from your harness.
-
Migration is small but not zero. 9 → 10 was ~1.5 days for us, mostly bumping target frameworks, package versions, and clearing new analyzer warnings. Budget for it; don't assume a clean find-and-replace.
-
Native AOT isn't free everywhere. It's a clear win for small, fast-start workers; for a big reflection-heavy app it can be more trouble than it's worth. We use AOT for workers and JIT for the main web app — pick per service.
-
What we'd do differently: we benchmarked 9 → 10 after upgrading. Better is what we do now — a nightly CI job that runs the BenchmarkDotNet harness against the next-version preview SDK, so the upgrade decision arrives with evidence already attached.
The closing mental model
Upgrade for banked, measured wins on the LTS; pilot the preview, don't deploy it. A .NET version bump is one of the highest-ROI changes you can make — free performance for a day or two of migration — if you take the LTS and verify with your own benchmarks. Version-chasing into production is the opposite: low reward, real risk.
Three habits this leaves you with:
- Ride LTS-to-LTS in production, pilot previews in CI. Stable where it counts, informed about what's next.
- Benchmark your own hot paths across frameworks with BenchmarkDotNet — the release notes are a starting hypothesis, your numbers are the answer.
- Adopt language features as cleanup, not as a project.
field, extension members, and friends pay for themselves the day you upgrade the compiler.
Further reading
- Scaling ASP.NET Core APIs to 100,000 Requests Per Minute — where the runtime/GC wins from this post fit into the bigger performance picture.
- 10 Hidden Memory Leaks in ASP.NET Core Applications — the GC/working-set work a version upgrade complements but doesn't replace.
- No Python, No PhD: Train Real ML Models in C# with ML.NET — the classical-ML side of the AI tooling in Section 6.
- Data Access in .NET — EF Core, LINQ, and Dapper — why the data layer, not the runtime, is usually the real bottleneck.
- Official .NET release notes & "What's new in .NET 10" / the .NET 11 preview announcements (verify any preview detail against the latest build).
Weighing a .NET upgrade and want a second opinion grounded in numbers? Email randhir.jassal@gmail.com with your current version and your hottest endpoint's p95, and I'll tell you whether the upgrade is likely to move your needle — or whether it's your code, not your runtime.
Get the next issue
A short, curated email with the newest posts and questions.