.NET 11 Preview 1 ships a groundbreaking feature: Runtime Async. Instead of relying solely on the C# compiler to rewrite async/await methods into state machines, the .NET runtime itself now understands async methods as a first-class concept. This article explores what Runtime Async is, why it matters, what changed in Preview 1, and how you can experiment with it today.
The Problem with Compiler-Generated Async
Since C# 5 introduced async/await, the compiler has been solely responsible for making async work. When you write an async method, the C# compiler rewrites it into a state machine—a generated struct implementing IAsyncStateMachine that tracks the method’s progress across suspension points.
This approach works well, but it comes with trade-offs:
- State machine overhead: Every
asyncmethod generates a state machine struct with fields for every local variable that lives across anawait. When the method doesn’t complete synchronously, this struct gets boxed onto the heap. - Debugger unfriendly: The generated state machine code makes stepping through async methods confusing. Stack traces show synthetic
MoveNextmethods instead of your original method names. - Profiler blind spots: Performance tools struggle to attribute time correctly across async suspension points because the runtime has no knowledge of the async semantics—it just sees regular method calls.
- Missed optimization opportunities: Because the runtime doesn’t understand the async pattern, it cannot optimize chains of async calls. Each
awaitforces a full suspension/resumption cycle even when the runtime could, in theory, optimize the handoff.
What Is Runtime Async?
Runtime Async moves the understanding of async methods from the compiler into the .NET runtime itself. Instead of the compiler generating a complex state machine, it emits simpler IL annotated with [MethodImpl(MethodImplOptions.Async)]. The runtime then takes responsibility for:
- Suspension and resumption: The runtime manages saving and restoring method state at suspension points, eliminating the need for compiler-generated state machine structs.
- Hoisting local variables: Only variables that actually live across suspension points are “hoisted” (preserved), and the runtime handles this directly.
- Optimized async-to-async calls: When one runtime-async method calls another, the runtime can optimize the handoff, potentially avoiding
Taskallocations entirely in chains of async calls.
How the New Model Works
In the Runtime Async model, async methods use AsyncHelpers to express suspension:
namespace System.Runtime.CompilerServices
{
public static class AsyncHelpers
{
[MethodImpl(MethodImplOptions.Async)]
public static void AwaitAwaiter<TAwaiter>(TAwaiter awaiter) where TAwaiter : INotifyCompletion;
[MethodImpl(MethodImplOptions.Async)]
public static void UnsafeAwaitAwaiter<TAwaiter>(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion;
[MethodImpl(MethodImplOptions.Async)]
public static void Await(Task task);
[MethodImpl(MethodImplOptions.Async)]
public static void Await(ValueTask task);
[MethodImpl(MethodImplOptions.Async)]
public static T Await<T>(Task<T> task);
[MethodImpl(MethodImplOptions.Async)]
public static T Await<T>(ValueTask<T> task);
[MethodImpl(MethodImplOptions.Async)]
public static void Await(ConfiguredTaskAwaitable configuredAwaitable);
[MethodImpl(MethodImplOptions.Async)]
public static void Await(ConfiguredValueTaskAwaitable configuredAwaitable);
[MethodImpl(MethodImplOptions.Async)]
public static T Await<T>(ConfiguredTaskAwaitable<T> configuredAwaitable);
[MethodImpl(MethodImplOptions.Async)]
public static T Await<T>(ConfiguredValueTaskAwaitable<T> configuredAwaitable);
}
}
When the C# compiler emits runtime-async IL, instead of building a full state machine, it generates a call to AsyncHelpers.Await(...). The runtime intercepts this, and if the task is not yet completed, suspends the method—saving only the necessary state—and resumes it later when the result is available.
What Changed in .NET 11 Preview 1
Runtime Async was first available for experimentation in .NET 10, but required setting environment variables to enable the runtime support. .NET 11 Preview 1 brings significant progress:
CoreCLR Support Enabled by Default
The CoreCLR runtime-async support is now enabled by default. You no longer need to set DOTNET_RuntimeAsync=1. The runtime is ready to execute runtime-async methods out of the box.
Native AOT Support
Preview 1 includes foundational Native AOT support for runtime-async methods. This means code compiled with runtime-async=on can now be ahead-of-time compiled, including continuation support and toolchain plumbing for diagnostics.

How to Experiment with Runtime Async
To try runtime-async in your own code with .NET 11 Preview 1:
1. Target .NET 11:
<TargetFramework>net11.0</TargetFramework>
2. Enable preview features and runtime-async compilation in your .csproj:
<PropertyGroup>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<Features>$(Features);runtime-async=on</Features>
</PropertyGroup>
3. Write async code as usual:
async Task<string> FetchDataAsync(HttpClient client, string url)
{
var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
With runtime-async enabled, the C# compiler generates simpler IL without a full state machine. The runtime handles suspension and resumption natively. Your source code doesn’t change at all—the improvement is entirely in how the code is compiled and executed.
Here is the .NET 11 Low-Level C# decompiled with JetBrains decompiler:
[NullableContext(1)]
[CompilerGenerated]
[MethodImpl(MethodImplOptions.Async)]
internal static Task<string> <<Main>$>g__FetchDataAsync|0_0(HttpClient client, string url)
{
HttpResponseMessage httpResponseMessage = AsyncHelpers.Await<HttpResponseMessage>(client.GetAsync(url));
httpResponseMessage.EnsureSuccessStatusCode();
return (Task<string>) AsyncHelpers.Await<string>(httpResponseMessage.Content.ReadAsStringAsync());
}
The generated IL is significantly simpler—no state machine struct, no MoveNext method. The runtime takes over the async orchestration, which opens the door for cross-method optimizations that are impossible with the compiler-only approach.
Compared to the complex state machine generated by the traditional .NET 10 compiler-async model:
[NullableContext(1)]
[AsyncStateMachine(typeof (Program.<<<Main>$>g__FetchDataAsync|0_0>d))]
[DebuggerStepThrough]
[CompilerGenerated]
internal static Task<string> <<Main>$>g__FetchDataAsync|0_0(HttpClient client, string url)
{
Program.<<<Main>$>g__FetchDataAsync|0_0>d stateMachine = new Program.<<<Main>$>g__FetchDataAsync|0_0>d();
stateMachine.<>t__builder = AsyncTaskMethodBuilder<string>.Create();
stateMachine.client = client;
stateMachine.url = url;
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start<Program.<<<Main>$>g__FetchDataAsync|0_0>d>(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
[CompilerGenerated]
private sealed class <<<Main>$>g__FetchDataAsync|0_0>d : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder<string> <>t__builder;
public HttpClient client;
public string url;
private HttpResponseMessage <response>5__1;
private HttpResponseMessage <>s__2;
private string <>s__3;
[Nullable(new byte[] {0, 1})]
private TaskAwaiter<HttpResponseMessage> <>u__1;
[Nullable(new byte[] {0, 1})]
private TaskAwaiter<string> <>u__2;
public <<<Main>$>g__FetchDataAsync|0_0>d()
{
base..ctor();
}
void IAsyncStateMachine.MoveNext()
{
int num1 = this.<>1__state;
string s3;
try
{
TaskAwaiter<HttpResponseMessage> awaiter1;
int num2;
TaskAwaiter<string> awaiter2;
if (num1 != 0)
{
if (num1 != 1)
{
awaiter1 = this.client.GetAsync(this.url).GetAwaiter();
if (!awaiter1.IsCompleted)
{
this.<>1__state = num2 = 0;
this.<>u__1 = awaiter1;
Program.<<<Main>$>g__FetchDataAsync|0_0>d stateMachine = this;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<HttpResponseMessage>, Program.<<<Main>$>g__FetchDataAsync|0_0>d>(ref awaiter1, ref stateMachine);
return;
}
}
else
{
awaiter2 = this.<>u__2;
this.<>u__2 = new TaskAwaiter<string>();
this.<>1__state = num2 = -1;
goto label_9;
}
}
else
{
awaiter1 = this.<>u__1;
this.<>u__1 = new TaskAwaiter<HttpResponseMessage>();
this.<>1__state = num2 = -1;
}
this.<>s__2 = awaiter1.GetResult();
this.<response>5__1 = this.<>s__2;
this.<>s__2 = (HttpResponseMessage) null;
this.<response>5__1.EnsureSuccessStatusCode();
awaiter2 = this.<response>5__1.Content.ReadAsStringAsync().GetAwaiter();
if (!awaiter2.IsCompleted)
{
this.<>1__state = num2 = 1;
this.<>u__2 = awaiter2;
Program.<<<Main>$>g__FetchDataAsync|0_0>d stateMachine = this;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, Program.<<<Main>$>g__FetchDataAsync|0_0>d>(ref awaiter2, ref stateMachine);
return;
}
label_9:
this.<>s__3 = awaiter2.GetResult();
s3 = this.<>s__3;
}
catch (Exception ex)
{
this.<>1__state = -2;
this.<response>5__1 = (HttpResponseMessage) null;
this.<>t__builder.SetException(ex);
return;
}
this.<>1__state = -2;
this.<response>5__1 = (HttpResponseMessage) null;
this.<>t__builder.SetResult(s3);
}
[DebuggerHidden]
void IAsyncStateMachine.SetStateMachine([Nullable(1)] IAsyncStateMachine stateMachine)
{
}
}
}
What’s Coming Next
The Runtime Async epic issue tracks remaining work for .NET 11:
- Library recompilation: Compiling
System.*and ASP.NET Core libraries with runtime-async support will unlock framework-wide performance gains. - Diagnostics improvements: Better stack trace formatting and printing for runtime-async methods.
- Testing: Enabling runtime-async compilation for unit tests and libraries across the ecosystem.
Once the core libraries are recompiled and shipped with runtime-async enabled, the full performance benefits—reduced allocations, smaller state, and optimized async call chains—should become measurable in real-world applications like ASP.NET Core.
Conclusion
Runtime Async is one of the most significant runtime-level changes in .NET’s history. By moving async understanding from a compiler rewrite into the runtime itself, .NET 11 sets the foundation for meaningful performance improvements across the entire async ecosystem, better debugging and profiling experiences, and future optimizations that are simply impossible with the compiler-only model.
While Preview 1 lays the groundwork with CoreCLR and Native AOT support, the real performance gains will materialize as the core libraries are recompiled with runtime-async in future previews. Now is a great time to experiment with the feature on your own code and get ready for the async revolution coming to .NET 11.
What are your thoughts on Runtime Async? Are you excited about the potential performance improvements? Let me know in the comments!