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;
}
Whether ConfigureAwait(false) is appropriate remains a design decision based on the library and execution environment.
Runtime Async improves the underlying async machinery; it does not change the semantics developers should understand when using await.
Cleaner Debugging and Stack Traces
Performance is not the only benefit.
Runtime Async also improves the runtime's understanding of asynchronous methods during debugging and diagnostics.
With traditional compiler-generated async code, live stack traces can expose state-machine infrastructure.
Conceptually, a call stack may contain entries related to:
AsyncMethod
AsyncMethodStateMachine
AsyncMethodBuilder
Continuation
Runtime Async can instead expose a cleaner logical call chain:
Controller
|
v
Service
|
v
Repository
This is useful when investigating production problems because developers spend less time navigating compiler-generated implementation details.
.NET 11 also improves debugging behavior around breakpoints and stepping through await boundaries.
It is important to distinguish this from exception stack traces. Runtime Async's most visible improvement is in live execution stacks and debugging/profiling views. Existing exception stack-trace handling already removes much of the compiler-generated infrastructure from normal exception traces.
A Practical ASP.NET Core Example
Consider a service that performs two independent I/O operations:
public async Task<CustomerSummary> GetSummaryAsync(int customerId)
{
var customer = await customerService.GetCustomerAsync(customerId);
var orders = await orderService.GetOrdersAsync(customerId);
return new CustomerSummary
{
CustomerId = customer.Id,
CustomerName = customer.Name,
OrderCount = orders.Count
};
}
The code remains exactly the kind of code you would write with Runtime Async enabled.
The improvement happens below the application code.
If the project contains:
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<Features>runtime-async=on</Features>
</PropertyGroup>
the compiler/runtime combination can use Runtime Async for supported methods.
The application does not need a custom Runtime Async API.
Should You Enable Runtime Async in Production?
This requires some caution.
Runtime Async is documented as a preview feature in .NET 11. That means production adoption should be evaluated according to the stability requirements of the application.
For an experimental or benchmark environment, enabling it is straightforward:
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<Features>runtime-async=on</Features>
</PropertyGroup>
For a production application, test the complete application rather than assuming that every async workload will automatically become faster.
Pay particular attention to:
Database-heavy endpoints.
HTTP client workloads.
Queue consumers.
Background services.
High-throughput APIs.
Applications using
AsyncLocal<T>.Custom middleware.
Libraries that implement custom task abstractions.
Diagnostic and profiling tools.
How to Measure the Difference
Do not decide whether Runtime Async is beneficial based on a single request-time measurement.
A better test should compare the same application with and without the feature.
For example, create two builds:
Build A
runtime-async disabled
Build B
runtime-async enabled
Then test the same workload.
Measure:
Metric | Why It Matters |
|---|---|
Requests/sec | Overall throughput |
Average latency | Typical request performance |
P95 latency | Tail behavior |
P99 latency | High-percentile behavior |
Allocations | Async allocation pressure |
GC collections | Memory-management impact |
CPU utilization | Runtime efficiency |
Working set | Memory consumption |
Error rate | Functional correctness |
Use a representative workload instead of a synthetic method containing only:
await Task.Delay(1);
A realistic ASP.NET Core test should include the actual database, HTTP, serialization, middleware, and application logic that your service normally executes.
Also run enough iterations for tiered compilation and application warm-up to stabilize.
Common Mistakes
Assuming runtime-async Changes C# Syntax
It does not.
You still write:
public async Task ProcessAsync()
{
await DoWorkAsync();
}
There is no new await syntax that application developers need to learn.
Expecting Every Method to Become Faster
Runtime Async provides runtime and JIT improvements, but application performance depends on the complete execution path.
If an endpoint spends most of its time waiting for a slow database query, reducing async overhead may have little effect on total request latency.
Removing async Everywhere
Do not rewrite application code simply because Runtime Async exists.
This:
public async Task<User> GetUserAsync()
{
return await repository.GetUserAsync();
}
may sometimes benefit from simplifying the forwarding path, but that does not mean every async method should be manually converted into a task-returning method.
Optimize based on profiling and readability.
Assuming Runtime Async Eliminates Allocations
It can reduce certain runtime allocations and overhead, but it cannot eliminate allocations caused by your own application objects, closures, collections, serialization, database providers, or other libraries.
Benchmarking Only Cold Starts
Runtime Async includes tiered compilation improvements. Measuring only the first few requests may produce misleading conclusions.
For server applications, benchmark both startup behavior and steady-state throughput.
Advantages of Runtime Async
Lower Async Overhead
Runtime-managed async execution gives the runtime more opportunities to optimize suspension, continuation, and resumption.
Better JIT Optimization
Tiered compilation and dedicated Runtime Async code generation allow hot asynchronous paths to receive additional optimization.
Cleaner Diagnostics
Live call stacks can represent actual async methods more directly instead of exposing compiler-generated state-machine infrastructure.
Reduced Continuation Overhead
The runtime can avoid unnecessary ExecutionContext capture and restore work when there is no relevant ambient state.
Framework-Level Benefits
.NET 11 runtime libraries use Runtime Async, so applications can benefit from framework code that has adopted the new model without rewriting their own application architecture.
Disadvantages and Considerations
It Is Still a Preview Feature
Runtime Async is not yet something to enable blindly across every production application.
Tooling and Compatibility Must Be Tested
Runtime Async changes how async methods are represented internally. Debuggers, profilers, analyzers, and other runtime-integrated tools should be tested with the application.
Performance Depends on Workload
A database-bound API may see a different benefit from an in-memory high-throughput service.
More Complex Runtime Internals
The new implementation adds another layer of runtime behavior that developers may encounter while diagnosing unusual async issues.
How to Opt Out
If a particular project needs to disable Runtime Async, .NET 11 provides a project-level option:
<PropertyGroup>
<UseRuntimeAsync>false</UseRuntimeAsync>
</PropertyGroup>
This is the supported project-level approach rather than relying on older environment-variable switches.
The older DOTNET_RuntimeAsync and UNSUPPORTED_RuntimeAsync environment variables are no longer the mechanism to use for controlling this behavior.
Runtime Async vs Traditional Async
Area | Traditional Compiler Async | Runtime Async |
|---|---|---|
State management | Compiler-generated state machine | Runtime-managed |
C# syntax |
| Same |
Application API |
| Same |
Suspension | Compiler-generated infrastructure | Runtime infrastructure |
JIT awareness | More indirect | More direct |
Tiered async optimization | Traditional model | Runtime Async participates |
Live stack traces | Can expose state-machine frames | Cleaner logical call chain |
Continuation optimization | Existing mechanisms | Additional runtime optimizations |
ASP.NET Core compatibility | Established | .NET 11 feature |
Adoption | Default traditional model | Opt-in for application compilation |
Best Practices
If you are evaluating Runtime Async in an ASP.NET Core application, follow these practices:
Keep normal
async/awaitcode. Runtime Async is designed to change the implementation underneath your source code.Enable it at the project level. Use the
runtime-async=onfeature for controlled testing.Benchmark realistic workloads. Include databases, HTTP calls, serialization, middleware, and application logic.
Measure allocations and GC. Async performance is not only about request latency.
Test diagnostic tooling. Check debugger behavior, profiling, tracing, and logging.
Pay attention to
AsyncLocal<T>. Context flow can influence async overhead.Do not optimize blindly. Profile before changing application-level async patterns.
Test before production adoption. Runtime Async is still a preview feature.
Compare steady-state performance. Allow tiered compilation and JIT optimizations to warm up.
Keep a rollback option. If a specific application or library shows unexpected behavior, project-level opt-out provides a practical fallback.
Conclusion
.NET 11's Runtime Async is a significant change in how .NET can implement async and await.
Instead of relying entirely on compiler-generated state machines, the runtime can manage asynchronous suspension and resumption directly. This gives the JIT and runtime more opportunities to optimize async execution, including tiered compilation, continuation reuse, tail-await optimizations, reduced ExecutionContext overhead, and smaller generated code.
For ASP.NET Core developers, the most important part is that the programming model does not fundamentally change. You still write familiar code:
public async Task<IActionResult> GetDataAsync()
{
var data = await service.GetDataAsync();
return Ok(data);
}
The difference is what happens underneath that code.
.NET 11 also brings Runtime Async into the runtime libraries and parts of the ASP.NET Core shared framework, making this more than an isolated compiler experiment.
However, Runtime Async should be evaluated with real workloads rather than assumed to be a universal performance switch. Measure throughput, latency, allocations, CPU, GC behavior, and diagnostic compatibility before adopting it broadly.
For applications that spend a significant amount of time executing asynchronous code, Runtime Async represents an important direction for the future of .NET async performance: making async/await something the runtime can understand and optimize directly rather than treating it primarily as compiler-generated infrastructure.

Join the conversation! Your thoughts help the community grow.