How Runtime Async Changes async/await in .NET 11

csharpdotnetperformance
How Runtime Async Changes async/await in .NET 11

.NET 11 introduces Runtime Async, a preview feature that lets the runtime handle suspension and resumption for eligible C# methods. The .NET runtime libraries already use it, but your application's methods use compiler-generated state machines until you opt in and rebuild. You can check which implementation a method uses by inspecting the flags on the compiled method.

What Happens When a Method Reaches an Incomplete Await?

A checkout has a subtotal of 1,200 cents. It needs to add the shipping charge before it can return the total. If the quote is ready when the method reaches await, execution can continue on the current thread. If the quote is pending, the method has to remember the subtotal and where to resume.

By default, the C# compiler generates a state machine for the method. It puts the subtotal in a field on that generated type and writes a MoveNext method that handles both the initial call and resumption. Open the app assembly in a decompiler with async reconstruction disabled and you can inspect that type.

With Runtime Async enabled, the compiler marks the method as async in its metadata and emits instructions that identify the await. The runtime can now decide which values to save at a suspension point. When one eligible async method directly awaits another, the JIT can pass the result directly to the caller without creating a separate task for that intermediate call.

Build the Same Method Twice

Start with a console app on the .NET 11 SDK. If you're upgrading an existing app, the .NET 11 release overview explains which changes you get by updating the runtime and which require a new build.

dotnet new console -n AsyncProbe -f net11.0
cd AsyncProbe

In Program.cs, use a TaskCompletionSource<int> for the shipping quote. The caller supplies the price only after calling TotalAsync, so the method always has to suspend at its await.

using System.Reflection;
using System.Runtime.CompilerServices;

var method = typeof(Shipping).GetMethod(nameof(Shipping.TotalAsync))!;
bool hasStateMachine = method.IsDefined(typeof(AsyncStateMachineAttribute));
bool usesRuntimeAsync = method.MethodImplementationFlags
    .HasFlag(MethodImplAttributes.Async);

Console.WriteLine($"Compiler state machine: {hasStateMachine}");
Console.WriteLine($"Runtime async flag: {usesRuntimeAsync}");

var quote = new TaskCompletionSource<int>(
    TaskCreationOptions.RunContinuationsAsynchronously);
Task<int> total = Shipping.TotalAsync(1_200, quote.Task);
Console.WriteLine($"Waiting for quote: {!total.IsCompleted}");

quote.SetResult(350);
Console.WriteLine($"Total in cents: {await total}");

public static class Shipping
{
    public static async Task<int> TotalAsync(int subtotal, Task<int> shippingQuote)
    {
        int shipping = await shippingQuote;
        return subtotal + shipping;
    }
}

Run dotnet run -c Release with the generated project file. The first line is True because AsyncStateMachineAttribute points to the type the compiler generated for Shipping.TotalAsync.

Compiler state machine: True
Runtime async flag: False
Waiting for quote: True
Total in cents: 1550

Now add this property group inside the Project element in AsyncProbe.csproj. Appending to $(Features) preserves any other compiler features already set for the project.

<PropertyGroup>
  <Features>$(Features);runtime-async=on</Features>
</PropertyGroup>

For a net11.0 project using the default C# version, this compiler switch is the only setting you need. You can leave EnablePreviewFeatures unset.

Run dotnet run -c Release again. The compiler has marked the method itself with the Async implementation flag.

Compiler state machine: False
Runtime async flag: True
Waiting for quote: True
Total in cents: 1550

In both builds the caller gets a pending Task<int>, supplies the shipping price, and awaits the same total. You can enable Runtime Async for the checkout project without recompiling a shipping library that already returns a compatible task.

Which Async Methods Can Use It?

Runtime Async supports async methods returning Task, Task<T>, ValueTask, or ValueTask<T>. Async iterators that return IAsyncEnumerable<T> still need compiler-generated machinery to handle yield return. The compiler also keeps the earlier implementation for async void and custom task-like return types.

Pause in the debugger while one eligible async method is calling another and inspect the live call stack. You can see the method names with fewer frames for the generated machinery. A logged exception can look much the same in both builds because .NET already cleans up exception stack traces.

How Should You Evaluate It in a Service?

Take a read path where a cache hit supplies a value immediately and a miss sends a database query. Run the same Release build configuration on .NET 11 with the feature off and on. Exercise hits and misses separately, then compare allocated bytes per request and request latency at the same concurrency. A cache-hit benchmark can't tell you how the call chain behaves after a real suspension.

If a miss spends most of its time waiting for the database, the round trip can dominate even when the app allocates less per request. If the service blocks threads with .Result or serializes independent queries, address those async/await performance pitfalls separately.

Summary

If an async method shows up in your allocation profile, build its project twice and check the Async flag before collecting timings. Keep the cache-hit rate the same for both runs. If one build hits the database more often, the latency numbers will reflect those extra queries.

Frequently Asked Questions

Is Runtime Async on by default in .NET 11?

The .NET runtime libraries ship with Runtime Async enabled. For application code, the feature is in preview and requires a compiler switch in the project you build.

Does Runtime Async require preview C# syntax?

No. A project targeting net11.0 can enable runtime-async=on in its Features property with the standard C# language version. The EnablePreviewFeatures property is also unnecessary.

Which method return types support Runtime Async in .NET 11?

Eligible methods return Task, Task<T>, ValueTask, or ValueTask<T>. Async iterators, async void methods, and methods with custom task-like return types still use compiler-generated state machines.

Can a Runtime Async method call a library built without the feature?

Yes. The public Task or ValueTask contract works across that boundary. You can enable Runtime Async in one project while continuing to use compatible packages compiled with the earlier implementation.

Browse all .NET guides →
Loading comments...

Whenever you're ready, there are 5 ways I can help you:

  1. Pragmatic Self-Hosting: Learn to deploy and run your apps on a VPS with Dokploy and Tailscale. Keep administration private, automate releases, and practice restoring backups, with source code and a runbook you can reuse.
  2. Pragmatic Clean Architecture: Join 5,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  3. Modular Monolith Architecture: Join 2,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  4. Pragmatic REST APIs: Join 1,900+ students in this course that will teach you how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  5. Patreon Community: Join a community of 5,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

The .NET Weekly

One practical .NET tip, every Saturday.

Join 66,000+ engineers who read it. Free, and you can unsubscribe any time.