Asynchronous programming has always been an important part of ASP.NET Core. Web applications spend a significant amount of time waiting for databases, HTTP APIs, file operations, queues, and other I/O resources. Using async and await allows the application to release threads while those operations are in progress.
However, the way C# traditionally implements async/await has an internal cost. The compiler transforms an async method into a state machine that stores the method's state across suspension points.
.NET 11 introduces Runtime Async, also called Runtime Async V2, which moves more of this responsibility from the C# compiler into the .NET runtime. Instead of relying on compiler-generated async state machines, the runtime manages suspension and resumption of async methods.
For ASP.NET Core applications, this is particularly interesting because many framework operations are already asynchronous. .NET 11 also compiles the runtime libraries with Runtime Async, allowing applications to benefit from the technology when they call those libraries.
The important point is that Runtime Async does not change how developers normally write asynchronous C# code. Your existing async and await syntax remains familiar. The major change happens underneath that source code.
What Is Runtime Async?
Traditionally, a method such as this:
public async Task<string> GetUserNameAsync(int userId)
{
var user = await GetUserAsync(userId);
return user.Name;
}
is transformed by the C# compiler into an async state machine.
Conceptually, the compiler needs to preserve information such as:
The current execution state.
Local variables that are needed after an
await.The awaiter.
The continuation.
The result of the operation.
Exception-handling state.
The generated implementation is an important part of how traditional .NET async programming works.
Runtime Async changes this model.
With Runtime Async enabled, the runtime has native infrastructure for managing async suspension and resumption. The compiler no longer needs to represent the complete async method using the traditional generated state-machine approach.
This provides the runtime with more opportunities to optimize async execution.
How Traditional async/await Works
Consider a simple ASP.NET Core endpoint:
app.MapGet("/users/{id:int}", async (int id, IUserService userService) =>
{
var user = await userService.GetUserAsync(id);
return Results.Ok(user);
});
The application code looks simple, but the compiler historically generates additional machinery behind the scenes.
The simplified execution flow is:
Request
|
v
Async Method
|
v
Start State Machine
|
v
Await Database/API Operation
|
v
Suspend
|
v
Operation Completes
|
v
Resume State Machine
|
v
Return Result
This design is highly optimized and has been used successfully for many years. Runtime Async does not mean that traditional async/await was inefficient or incorrect.
Instead, .NET 11 provides another implementation strategy that allows the runtime and JIT to understand async execution more directly.
What Changes with runtime-async?
Runtime Async is enabled at compilation through the runtime-async feature switch.
For a .NET 11 project, the project file can include:
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<Features>runtime-async=on</Features>
</PropertyGroup>
The application source code does not need to be rewritten.
For example:
public async Task<Order> GetOrderAsync(int orderId)
{
var order = await database.GetOrderAsync(orderId);
return order;
}
The important difference is how the runtime executes the method rather than how you write it.
This distinction matters because developers should not expect to replace await with a new syntax.
Runtime Async is an implementation and runtime optimization rather than a new programming model.
Runtime Async and ASP.NET Core
ASP.NET Core is heavily dependent on asynchronous operations.
Typical request processing may involve:
HTTP Request
|
v
ASP.NET Core Middleware
|
v
Authentication
|
v
Authorization
|
v
Controller / Minimal API
|
v
Database / HTTP / File / Queue
|
v
HTTP Response
Many operations in this pipeline are asynchronous.
.NET 11 also compiles relevant runtime libraries with Runtime Async. ASP.NET Core shared-framework libraries have also adopted the feature for .NET 11-compatible scenarios.
This means applications can receive runtime-level improvements when they call framework APIs even if developers do not manually convert every application method.
For example:
app.MapGet("/products", async (ProductDbContext db) =>
{
var products = await db.Products
.AsNoTracking()
.ToListAsync();
return Results.Ok(products);
});
There is no special ASP.NET Core version of await.
The application still uses normal C# asynchronous programming while the underlying runtime and framework infrastructure can use Runtime Async where supported.
Runtime Async Performance Improvements
The main benefit is not a single optimization. .NET 11 introduces several improvements around Runtime Async.
Dedicated Runtime Async JIT Compilation
The JIT can compile a dedicated Runtime Async version of a synchronous task-returning method instead of relying on an additional thunk layer.
This can reduce unnecessary indirection when a method is used in an async context.
For example:
public Task<string> GetCachedValueAsync()
{
return Task.FromResult("cached");
}
Methods that complete synchronously are particularly interesting because the runtime can recognize common task-returning patterns.
.NET 11 also improves JIT recognition of common factories such as:
Task.CompletedTask
Task.FromResult(value)
ValueTask.FromResult(value)
This helps the runtime optimize common fast paths.
Tiered Compilation for Async Methods
Another important change is async methods participating in tiered compilation.
Tiered compilation allows the runtime to initially compile methods quickly and later generate more optimized code for methods that become hot.
Conceptually:
Application Starts
|
v
Initial Compilation
|
v
Method Executes
|
v
Method Becomes Hot
|
v
Tier 1 Optimization
|
v
Optimized Execution
Previously, Runtime Async methods did not benefit from the same tiering path.
.NET 11 changes this behavior so frequently executed async methods can receive further JIT optimization after the application warms up.
This is particularly relevant for server applications where the same request paths can execute thousands or millions of times.
Tail-Await Optimizations
.NET 11 also improves the handling of async methods that directly return another asynchronous operation.
Consider:
public async Task<Customer> GetCustomerAsync(int id)
{
return await repository.GetCustomerAsync(id);
}
There may be cases where the intermediate async method provides little value.
The runtime can now make better use of tail-call and tail-await behavior for suitable patterns.
In other words, the runtime can reduce unnecessary work when one asynchronous operation simply passes another asynchronous result through.
This does not mean every async method should be rewritten to remove await.
If the method needs exception handling, logging, transformations, multiple awaits, or other logic, keeping the normal async implementation is usually clearer.
Tail-Merged Suspension Points
Async methods can contain multiple suspension points:
public async Task ProcessAsync()
{
await StepOneAsync();
await StepTwoAsync();
await StepThreeAsync();
}
Each suspension point represents a place where execution can stop and later continue.
.NET 11 includes JIT work that can tail-merge async suspension points. The objective is to reduce generated code size by sharing suitable portions of the generated execution paths.
This matters because async-heavy applications can contain large numbers of methods with multiple awaits.
Reducing generated code can also help the runtime's code-generation and instruction-cache behavior, although the actual impact depends on the application.
Cached Continuations
A continuation represents work that needs to resume after an asynchronous operation completes.
Runtime Async improves continuation handling by allowing suitable continuation objects to be cached and reused.
The goal is to reduce unnecessary allocation pressure in async-heavy workloads.
This is particularly relevant for high-throughput services where the same asynchronous execution patterns are repeated continuously.
However, developers should not interpret this as a reason to ignore application-level allocations.
Code such as this can still create unnecessary work:
public async Task<string> ProcessAsync()
{
var data = new LargeObject();
await SomeOperationAsync();
return data.ToString();
}
Runtime-level improvements cannot eliminate allocations that your application logic genuinely requires.
ExecutionContext and Async Performance
One of the more interesting .NET 11 changes concerns ExecutionContext.
ExecutionContext is used to flow ambient execution information across asynchronous continuations. AsyncLocal<T> is one common mechanism that participates in this flow.
For example:
private static readonly AsyncLocal<string?> RequestId = new();
public async Task ProcessAsync()
{
RequestId.Value = "request-123";
await Task.Delay(10);
Console.WriteLine(RequestId.Value);
}
The runtime needs to preserve the appropriate execution context across the asynchronous boundary.
Historically, continuation processing could involve capturing and restoring context even when there was no meaningful ambient state to restore.
.NET 11 improves this path.
When there is no relevant context state, the runtime can avoid unnecessary capture and restore work.
This benefits:
TaskTask<T>ValueTaskValueTask<T>Runtime Async continuations
The improvement is especially relevant to high-throughput asynchronous applications.
Does ConfigureAwait(false) Become Unnecessary?
No.
Runtime Async does not make ConfigureAwait(false) universally unnecessary.
In ASP.NET Core applications, there is generally no UI synchronization context like the one found in traditional desktop application models. However, library code can still have synchronization-context considerations depending on where it runs.
For example:
public async Task<string> LoadDataAsync()
{
var response = await httpClient.GetStringAsync(
"api/data").ConfigureAwait(false);
return response;
}
Join the conversation! Your thoughts help the community grow.