.NET 11 is faster than .NET 10 almost everywhere, and most of the gain arrives without a code change. You retarget to net11.0, rebuild, and the new JIT, runtime and libraries do the rest.
The work falls into three buckets. The JIT devirtualizes more, allocates less behind your back and removes many more bounds checks. Runtime async moves async state machines out of the C# compiler and into the JIT. And the libraries, from regex, LINQ and collections to JSON, networking, Base64, numerics and cryptography, picked up hundreds of targeted fixes. Microsoft's microbenchmarks show many hot paths running 2x to 10x faster, and a few regex patterns more than 50x faster.
As of October 2026, .NET 11 is at Release Candidate 3 with a go-live license, and general availability is planned for .NET Conf 2026 on November 10, 2026. It is a Standard Term Support (STS) release, supported until November 9, 2028.
Where the numbers come from, and how far to trust them
Every year Stephen Toub publishes a very long post on the .NET Blog covering the performance work in the coming release. This year's, Performance Improvements in .NET 11, appeared on September 15, 2026 and covers hundreds of pull requests. The figures below come from that post and from Microsoft's What's new in the .NET 11 runtime documentation.
They are microbenchmarks on Microsoft's hardware, comparing .NET 10 and .NET 11 release candidates. Read them as a map of where the speed comes from, not as a forecast for your application:
Microbenchmark gain (one method, 2x to 50x)
↓
Only matters if that method is on your hot path
↓
Real app gain = sum of small wins across JIT, GC, libraries
↓
Usually a few percent to a few tens of percent in throughput or allocations
The JIT: paying less for abstractions
The JIT has the broadest reach of anything in this release, because every method in your app goes through it. The .NET 11 work splits into deabstraction, bounds-check elimination and better code generation.
Devirtualization and escape analysis
Interfaces, generics, IEnumerable<T> and boxing used to cost a virtual call or a heap allocation. .NET 11 removes more of that cost. Generic virtual methods (GVMs) can now be devirtualized, including shared GVMs and default interface methods on generic interfaces. Boxing a nullable to call an interface method no longer allocates when the box doesn't escape. Enumerator chains, such as the read-only wrappers that collection expressions create, can be stack-allocated during foreach. And the result of Activator.CreateInstance<T>() is treated as an exact type, so later calls on it can be devirtualized and inlined.
This benchmark follows the shape of the examples in Microsoft's post and shows GVM devirtualization:
// dotnet run -c Release -f net10.0 --filter "*" --runtimes net10.0 net11.0
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkSwitcher.FromAssembly(typeof(GvmBenchmarks).Assembly).Run(args);
[MemoryDiagnoser(false)]
[HideColumns("Job", "Error", "StdDev", "Median", "RatioSD")]
public class GvmBenchmarks
{
private readonly IFormatterService _service = new JsonishFormatter();
[Benchmark]
public string FormatInt() => _service.Format(42);
}
public interface IFormatterService
{
string Format<T>(T value);
}
public sealed class JsonishFormatter : IFormatterService
{
public string Format<T>(T value) => value?.ToString() ?? "null";
}
In Microsoft's runs, the equivalent non-shared GVM call fell from 6.678 ns and 24 bytes allocated to 1.764 ns and zero bytes. The shared case fell from 7.166 ns to 1.764 ns.
The nullable-boxing fix is just as visible. Passing an int? into a generic method that tests value is IFormattable and calls ToString on it used to box:
private int? _nullableValue = 42;
[Benchmark]
public string? FormatNullableInt() => Format(_nullableValue);
private static string? Format<T>(T value)
{
if (value is IFormattable formattable)
return formattable.ToString(null, null);
return null;
}
On .NET 10 that took 9.583 ns and allocated 24 bytes. On .NET 11 it takes 1.987 ns and allocates nothing.
Bounds-check elimination
Every array or span access normally carries a check that the index is in range, and the JIT drops it only when it can prove the index is safe. .NET 11 teaches it dozens of new proofs:
i + constant < length patterns, and lookahead reads such as span[i + 1] after a (uint)(i + 2) < span.Length check.
- Index-from-end access such as
values[^1].
- The first element after an empty-span guard:
if (!span.IsEmpty && span[0] == value).
- Several checks in the same block coalesced into one, which took a 16-element sum from 2.958 ns to 1.828 ns.
- Overflow checks in
checked code removed when range analysis proves there is no overflow.
Code generation
Smaller wins add up here. Half arithmetic and conversions use hardware FP16 instructions where available (HalfToSingle went from 2.506 ns to 1.380 ns). On x64, Math.BigMul(long, long, out long) compiles to a single MUL. A 16 KB stackalloc is cheaper to set up, 162.66 ns instead of 312.97 ns. And the JIT can constant-fold string.Equals and SequenceEqual when both sides are known literals, which matters more than it sounds once inlining has exposed them.
Runtime async, the headline feature
Since C# 5, the compiler has turned every async method into a hidden state-machine class. In .NET 11 that job can move to the runtime: the compiler emits a much smaller IL contract and marks the method as async in metadata, and the JIT generates the suspend and resume logic itself. You get less code, fewer allocations on fast paths, cheaper exception propagation and far cleaner stack traces in profilers and debuggers.
It is still labelled a preview feature in .NET 11, but a net11.0 project no longer needs EnablePreviewFeatures. Turn it on with a feature flag in the project file:
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<Features>$(Features);runtime-async=on</Features>
</PropertyGroup>
There is no new syntax; existing async/await code simply compiles the new way. The .NET 11 libraries already ship compiled with runtime async, so async methods inside the BCL use it whether or not your projects opt in. To opt a project out explicitly, set <UseRuntimeAsync>false</UseRuntimeAsync>. The old DOTNET_RuntimeAsync environment variable is gone.
Microsoft compared a two-level await chain compiled the classic way against runtime async, both on .NET 11:
Scenario | Classic (compiler state machine) | Runtime async | Ratio |
Awaiting an already-completed task | 21.221 ns, 144 B | 6.151 ns, 0 B | 0.29 |
Awaiting Task.Yield() | 254.139 ns, 248 B | 116.927 ns, 168 B | 0.46 |
Exception through 10 frames, no yield | 19.469 μs, 15.13 KB | 5.885 μs, 2.13 KB | 0.30 |
Exception through 30 frames, with yield | 65.974 μs, 85.53 KB | 11.254 μs, 7.72 KB | 0.17 |
In a ten-layer test app, the async code shrank from 10,752 bytes to 5,632 bytes.
There are limits. async void methods, async iterators (IAsyncEnumerable<T> with yield return) and custom task-like builders aren't handled yet and keep the compiler-generated state machine. Microsoft's stated goal for .NET 11 is behavioural compatibility and at least parity with .NET 10; turning it on by default is a hope for .NET 12, not a commitment. My advice is to try it in a branch, compare behaviour and numbers, and ship it only when your tests agree. Keep choosing Task or ValueTask on API design grounds, and don't contort code to avoid await.
One related change helps everyone, opted in or not: continuations now skip ExecutionContext capture and restore when there is no ambient state, such as AsyncLocal<T>, to carry.
GC and memory
There is no new GC mode. DATAS (Dynamic Adaptation To Application Sizes), on by default for Server GC since .NET 9, carries on unchanged. The gains are in work the GC and JIT do together.
Storing a reference into an array of a reference type needs a covariance check and a write barrier, and both got cheaper: a covariant store loop went from 10.85 μs to 6.04 μs, and copying whole structs that contain references went from 4.13 ns to 3.07 ns. The bigger change is dependent-handle aging. Young-generation collections can now skip old dependent handles, so with 1,000,000 handles a Gen0 collection dropped from 10.737 ms to 310.7 μs. If you lean on ConditionalWeakTable, that is a real win.
The runtime can also now start on machines with more than 1,024 logical processors, though the GC keeps its 1,024-heap limit.
LINQ and collections
Collections and LINQ received a long list of targeted fixes, many from community contributors:
Operation | .NET 10 | .NET 11 | Ratio |
source.Append(6).Skip(10).LastOrDefault()
| 44.89 ns, 144 B | 16.32 ns, 112 B | 0.36 |
Max over 64 bytes
| 7.827 ns | 2.059 ns | 0.26 |
HashSet.UnionWith into a fresh set
| 46.207 μs, 252 KB | 2.433 μs, 76 KB | 0.05 |
ImmutableSortedSet.SetEquals (10,000 items)
| 767.7 μs, 430 KB | 117.6 μs, 0 B | 0.15 |
Span.Sort with a struct comparer
| 11.62 μs, 88 B | 3.533 μs, 0 B | 0.30 |
ToFrozenDictionary allocations
| 347 KB | 127 KB | 0.37 |
.NET 11 also adds FullJoin, plus tuple-returning Join and GroupJoin overloads, on Enumerable, Queryable and AsyncEnumerable. It reads better than the old hand-written composition and runs much faster: on 10,000 elements per side, the manual GroupJoin + SelectMany + Concat version took 27.6 ms and 3.23 MB, while FullJoin took 1.7 ms and 1.56 MB.
var products = new List<(int Id, string Name, string? Category)>
{
(1, "Laptop", "Electronics"),
(2, "Orphan", null),
};
var categories = new List<(string Name, string Description)>
{
("Electronics", "Electronic devices"),
("Furniture", "Office furniture"),
};
foreach (var (product, category) in products.FullJoin(categories, p => p.Category, c => c.Name))
{
Console.WriteLine($"{product.Name ?? "(none)"}: {category.Description ?? "(none)"}");
}
// Laptop: Electronic devices
// Orphan: (none)
// (none): Office furniture
Regex, JSON and text
The largest ratios in the whole release are in regex, and they come from smarter prefix analysis that lets the engine jump straight to candidate matches with vectorized search:
Pattern type | .NET 10 | .NET 11 |
Ignore-case alternation | 415.0 μs | 7.012 μs |
Ignore-case prefix inside a capture group | 277.7 μs | 7.286 μs |
Alternation that maps to SearchValues<string> | 861.9 μs | 9.251 μs |
Backreference comparison | 168.4 ns | 49.59 ns |
Those numbers apply to specific pattern shapes, so test your own patterns before celebrating.
In System.Text.Json, writing strings that need escaping is up to about 4x faster (30.83 μs to 7.875 μs for a fully escaped 2,048-character string), and the reader skips whitespace runs with vectorized search (7.418 μs to 5.945 μs). Elsewhere in text handling, in-place Base64 decoding went from 10.70 μs to 2.256 μs and TimeZoneInfo.ConvertTimeFromUtc from 45.13 ns to 19.44 ns.
Networking
The most dramatic networking number is for a broken network. A new ConnectAlgorithm.Parallel races IPv4 and IPv6 addresses, so with a stalled IPv6 route a connection took 1.06 ms instead of 511 ms. It is opt-in.
On Linux, OpenSSL now reads and writes straight into managed buffers through a custom BIO, taking a 16 KB TLS 1.3 round trip from 68.04 μs to 57.39 μs. HttpClient precomputes its Accept-Encoding header, so a GetAsync allocates 2.87 KB instead of 3.44 KB, and MultipartContent serialization is about twice as fast. Uri parsing and normalization are 30% to 50% faster with roughly half the allocations in common cases.
Two additions are worth knowing about: SocketsHttpHandler.InitialHttp2MaxConcurrentStreams, which limits HTTP/2 concurrency before the server's SETTINGS frame arrives, and Zstandard compression in System.IO.Compression.
ASP.NET Core 11
Microsoft's performance post is about the runtime and libraries, and Microsoft hasn't published ASP.NET Core-specific throughput numbers for .NET 11. ASP.NET Core still inherits most of the gains above for free, because Kestrel, routing, JSON, logging and HttpClient sit on the same runtime pieces.
ASP.NET Core 11 also adds features that cut work per request. [ShortCircuit] on a minimal API endpoint runs it right after routing and skips the rest of the middleware pipeline, which is ideal for health checks and robots.txt:
var app = WebApplication.CreateBuilder(args).Build();
// Runs right after routing; middleware registered later in the pipeline is skipped
app.MapGet("/health", [ShortCircuit] () => "Healthy");
app.Run();
On the Blazor side, CacheView caches the rendered output of a component subtree in static SSR, so child components don't run on a cache hit. Blazor Server can now pause inactive tabs and release their server memory and SignalR connections (opt-in), and Blazor WebAssembly can trim away unused metrics, tracing and hot-reload code. One change goes the other way: the default OverscanCount for Virtualize rose from 3 to 15, so more rows render by default. If you tuned large virtualized lists, check them after upgrading.
Startup and deployment
Precompiled ReadyToRun framework code on Windows and Linux x64 now targets x86-64-v3 (AVX2, BMI, FMA and more). Comparer<T>.Default and EqualityComparer<T>.Default are now specialized in R2R images, so collection code no longer falls back to the JIT at startup; Microsoft reports up to 20x gains in the affected operations. NativeAOT routes interface dispatch through a shared, patchable helper, which shrinks call sites and speeds up interface-heavy code.
Measuring the gains in your own app
Don't upgrade on the strength of someone else's benchmarks. Measure your own hot paths, in three layers.
1. Benchmark hot methods with BenchmarkDotNet
Create a benchmark project that targets both runtimes. The stable BenchmarkDotNet 0.15.8 doesn't recognise the net11.0 moniker; Microsoft's post uses 0.16.0-preview.1, and 0.16.0-preview.2 is also available.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net11.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.16.0-preview.2" />
<ProjectReference Include="..\MyApp.Core\MyApp.Core.csproj" />
</ItemGroup>
</Project>
Point the benchmarks at your code, such as the serializer settings, query builders or parsers you actually use:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Text.Json;
BenchmarkSwitcher.FromAssembly(typeof(OrderBenchmarks).Assembly).Run(args);
[MemoryDiagnoser]
public class OrderBenchmarks
{
private Order _order = null!;
private string _json = "";
[GlobalSetup]
public void Setup()
{
_order = Order.CreateSample(lines: 50);
_json = JsonSerializer.Serialize(_order);
}
[Benchmark]
public string Serialize() => JsonSerializer.Serialize(_order);
[Benchmark]
public Order? Deserialize() => JsonSerializer.Deserialize<Order>(_json);
}
Then run both runtimes side by side from the same build:
dotnet run -c Release -f net10.0 --filter "*" --runtimes net10.0 net11.0
BenchmarkDotNet prints one row per runtime with Mean, Ratio and Allocated columns, and writes Markdown, CSV and HTML reports to BenchmarkDotNet.Artifacts. If you need a refresher on writing benchmarks that mean something, Performance Benchmarking With BenchmarkDotNet covers the basics.
2. Compare runtime counters under load
Microbenchmarks can't show GC pauses, thread-pool behaviour or lock contention. Run the real app on .NET 10 and .NET 11 on the same machine and watch the counters:
dotnet tool install --global dotnet-counters
dotnet-counters monitor --name MyApi --counters System.Runtime
At the same request rate, compare CPU, allocation rate, GC count per generation, time in GC, working set and thread-pool queue length.
3. Load test end to end
Use your usual tool, whether k6, JMeter, bombardier or Microsoft's crank for ASP.NET Core, with an identical script against both builds. Record requests per second at a fixed CPU budget; p50, p95 and p99 latency at a fixed request rate; memory after warm-up; and cold-start time if you deploy to containers or serverless.
Most bad comparisons come from the same few mistakes: a Debug build or an attached debugger, runs on different machines or instance types, a single run with no look at variance, or measuring only the first few seconds before tiered compilation and Dynamic PGO have reached optimized code. And if you turn on runtime async, don't change anything else in the same test.
Should you upgrade, and how
Your situation | Recommendation |
On .NET 8 or .NET 9 | Both reach end of support on November 10, 2026, so move now. .NET 10 LTS is the conservative choice; .NET 11 is fine if you can upgrade again within two years. |
On .NET 10 LTS, stability first | Stay on .NET 10 (supported until November 14, 2028) and test .NET 11 in a branch. |
On .NET 10, performance-sensitive | Benchmark .NET 11 now on RC3 (go-live), and adopt at GA if your numbers justify it. |
Running on very old x86 hardware | Check the new CPU baseline first. |
That last row is the one most likely to bite. .NET 11 requires x86-64-v2 on x86/x64, which adds SSE4.2, POPCNT, CX16 and friends, and LSE on Windows Arm64. On an older CPU the app fails at startup with "The current CPU is missing one or more of the baseline instruction sets." Check your hosts, including the ones under your containers, before anything else.
After that, the upgrade is routine:
- Install the .NET 11 SDK and pin it with
global.json so CI and developers build with the same version.
- Retarget each project to
<TargetFramework>net11.0</TargetFramework> and move Microsoft.* and System.* packages to 11.0 versions.
- Review breaking changes; How to Find Breaking Changes Before Upgrading to .NET 11 lays out a systematic approach.
- Run the full test suite, including integration tests against real databases and services.
- Benchmark and load test using the three layers above.
- Try runtime async in a separate branch and compare throughput, allocations, binary size and stack traces.
- Move container base images to the 11.0 tags.
For most teams on .NET 10, I'd do the benchmarking now on RC3 and make the decision at GA with your own numbers in hand. The free JIT and library gains are real, but the STS support window ending November 9, 2028 is the trade you are making for them.
Join the conversation! Your thoughts help the community grow.