.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.

