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:

  • Task

  • Task<T>

  • ValueTask

  • ValueTask<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

async / await

Same

Application API

Task / ValueTask

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:

  1. Keep normal async/await code. Runtime Async is designed to change the implementation underneath your source code.

  2. Enable it at the project level. Use the runtime-async=on feature for controlled testing.

  3. Benchmark realistic workloads. Include databases, HTTP calls, serialization, middleware, and application logic.

  4. Measure allocations and GC. Async performance is not only about request latency.

  5. Test diagnostic tooling. Check debugger behavior, profiling, tracing, and logging.

  6. Pay attention to AsyncLocal<T>. Context flow can influence async overhead.

  7. Do not optimize blindly. Profile before changing application-level async patterns.

  8. Test before production adoption. Runtime Async is still a preview feature.

  9. Compare steady-state performance. Allow tiered compilation and JIT optimizations to warm up.

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