.NET  

.NET 11 Runtime Async Performance: What Actually Changes

Asynchronous programming is fundamental to modern .NET applications.

ASP.NET Core applications use asynchronous I/O to handle concurrent requests. Background workers use Task and ValueTask to coordinate operations. Database providers, HTTP clients, file APIs, and messaging libraries all depend heavily on asynchronous execution.

For years, the C# compiler has transformed an async method into a state machine that allows execution to pause at an await and resume later.

.NET 11 introduces a significant change to that model with Runtime Async, also referred to as Runtime Async V2.

Instead of relying entirely on compiler-generated async state machines, the runtime can manage async suspension and resumption itself. Microsoft documents improvements in stack traces, debugging, continuation handling, JIT integration, and async overhead. Runtime Async is still a preview feature, so the correct way to evaluate it is through controlled benchmarks rather than assuming every asynchronous workload will automatically become faster.

What Changes With Runtime Async?

Traditional C# async compilation transforms code conceptually like this:

public async Task<string> GetDataAsync()
{
    var data = await LoadDataAsync();
    return data.ToUpperInvariant();
}

into compiler-generated machinery that tracks the current execution state.

Conceptually:

Async Method
     |
     v
Compiler-generated State Machine
     |
     +---- Await
     |
     +---- Suspension
     |
     +---- Continuation

Runtime Async changes where much of this responsibility lives:

Async Method
     |
     v
Runtime-managed Async Execution
     |
     +---- Suspension
     |
     +---- Continuation
     |
     +---- Resumption

The goal is to reduce unnecessary overhead while making asynchronous execution easier to inspect and debug.

Microsoft describes Runtime Async as a step toward replacing compiler-generated async state machines with runtime-managed suspension and resumption.

Runtime Async Is a Preview Feature

Before testing it, understand its current status.

.NET 11 is still in preview, and Runtime Async is also a preview feature. To explicitly enable it in an application, add the following to the project file:

<PropertyGroup>
  <TargetFramework>net11.0</TargetFramework>
  <Features>runtime-async=on</Features>
</PropertyGroup>

For net11.0 projects, Microsoft notes that EnablePreviewFeatures is no longer required specifically for Runtime Async.

The runtime libraries themselves are compiled with Runtime Async enabled, providing broad functional and performance validation of the feature. Microsoft specifically invites feedback about throughput and library-size changes observed by developers.

Why Async Performance Is Difficult to Benchmark

It is tempting to benchmark two methods and conclude:

.NET 10 async = X
.NET 11 async = Y

That is not enough.

Async performance depends on several factors:

  • Whether the operation completes synchronously.

  • Whether an actual suspension occurs.

  • Whether the operation is CPU-bound or I/O-bound.

  • Whether ExecutionContext is captured.

  • Whether AsyncLocal<T> is used.

  • Whether Task or ValueTask is returned.

  • Whether the method is JIT compiled.

  • Whether ReadyToRun is involved.

  • Whether NativeAOT is involved.

  • How frequently the operation executes.

  • How much work happens outside the async infrastructure.

A good benchmark should therefore isolate individual scenarios.

Scenario 1: Synchronous Completion

The first test should measure an async method where the awaited operation is already complete.

public static async Task<int> GetValueAsync()
{
    return await Task.FromResult(42);
}

A simpler synchronous method provides a baseline:

public static int GetValue()
{
    return 42;
}

The purpose is not to claim that asynchronous code should replace synchronous code.

Instead, this test helps determine how much overhead exists when an asynchronous method takes a synchronous fast path.

Runtime Async specifically includes JIT support for task-returning methods that complete synchronously. Microsoft reports that the JIT can compile a dedicated Runtime Async version instead of routing through an additional thunk.

Scenario 2: Actual Suspension

A more realistic asynchronous benchmark needs an actual suspension point.

For example:

public static async Task<int> GetDelayedValueAsync()
{
    await Task.Delay(1);
    return 42;
}

This introduces a real asynchronous continuation.

However, Task.Delay also introduces timer and scheduling behavior that can dominate the measurement.

For runtime-focused benchmarking, a custom asynchronous producer or controlled synchronization mechanism can sometimes provide a cleaner experiment.

The important principle is to understand what the benchmark is actually measuring.

Scenario 3: Multiple Await Points

Real applications often contain several asynchronous operations:

public static async Task<string> ProcessAsync()
{
    var customer = await LoadCustomerAsync();
    var orders = await LoadOrdersAsync(customer.Id);
    var result = await BuildResultAsync(customer, orders);

    return result;
}

This creates multiple suspension points.

Runtime Async introduces tail merging of async suspension points, which can reduce generated code size. Microsoft lists this among the Runtime Async performance improvements in the later .NET 11 previews.

A benchmark should therefore compare:

One await
Two awaits
Three awaits
Many awaits

rather than assuming that every method benefits equally.

Scenario 4: ExecutionContext

ExecutionContext carries ambient execution state across asynchronous boundaries.

One example is AsyncLocal<T>:

private static readonly AsyncLocal<string?> RequestId
    = new();

public static async Task ProcessAsync()
{
    RequestId.Value = "request-123";

    await Task.Delay(1);

    Console.WriteLine(RequestId.Value);
}

The runtime needs to preserve the appropriate execution context when continuations resume.

.NET 11 includes an optimization where async continuations can avoid unnecessary ExecutionContext capture and restoration when there is no ambient state that needs to be propagated. Microsoft states that Task, Task<T>, ValueTask, and ValueTask<T> benefit from this behavior.

This is particularly relevant to high-throughput services where asynchronous operations occur millions of times.

However, do not interpret this as meaning that AsyncLocal<T> is always expensive or that developers should remove it indiscriminately.

Measure the actual application.

Scenario 5: Task vs ValueTask

A benchmark should also distinguish between Task and ValueTask.

For example:

public static Task<int> GetTaskAsync()
{
    return Task.FromResult(42);
}

and:

public static ValueTask<int> GetValueTaskAsync()
{
    return ValueTask.FromResult(42);
}

These are not interchangeable performance optimizations.

ValueTask<T> can be useful when an operation frequently completes synchronously and allocation behavior matters, but it also has usage constraints.

Do not change every Task<T> API to ValueTask<T> simply because a benchmark shows a difference in one microbenchmark.

The API's consumption pattern matters.

Building a BenchmarkDotNet Test

BenchmarkDotNet is useful for comparing asynchronous methods under controlled conditions.

A simple benchmark might look like this:

using BenchmarkDotNet.Attributes;

[MemoryDiagnoser]
public class AsyncBenchmarks
{
    [Benchmark]
    public async Task<int> TaskFromResult()
    {
        return await Task.FromResult(42);
    }

    [Benchmark]
    public int Synchronous()
    {
        return 42;
    }
}

Run it in Release configuration:

dotnet run -c Release

The benchmark should be executed separately under each runtime configuration being compared.

Do not mix Debug and Release results.

What Metrics Should You Capture?

A meaningful async benchmark should collect more than execution time.

MetricWhy It Matters
MeanAverage operation cost
ErrorMeasurement uncertainty
Standard deviationRun-to-run variation
Allocated bytesMemory pressure
GC collectionsGarbage-collection impact
ThroughputUseful for server workloads
Code sizeRelevant to runtime-generated machinery
Stack behaviorUseful for diagnostics

BenchmarkDotNet's memory diagnoser can help measure allocation behavior:

[MemoryDiagnoser]
public class AsyncBenchmarks
{
    // Benchmarks
}

If the feature is intended to reduce overhead, allocations should be measured alongside latency.

Cleaner Stack Traces

Performance is not the only change.

Runtime Async also changes how live stack traces appear.

With traditional compiler-generated async state machines, a live stack can expose compiler-generated infrastructure.

Runtime Async allows the actual asynchronous methods to appear more directly in the live stack.

For example:

static async Task OuterAsync()
{
    await MiddleAsync();
}

static async Task MiddleAsync()
{
    await InnerAsync();
}

static async Task InnerAsync()
{
    await Task.CompletedTask;

    Console.WriteLine(
        new System.Diagnostics.StackTrace());
}

Microsoft documents cleaner live stack traces as one of the visible Runtime Async improvements.

This matters for:

  • Debuggers

  • Profilers

  • Diagnostic tooling

  • Application logging

  • Production troubleshooting

It is important to distinguish live stack traces from exception stack traces.

Microsoft notes that exception stack traces already receive cleanup from existing ExceptionDispatchInfo behavior, so the improvement is primarily visible when inspecting the live execution stack.

Debugging Async Code

Async debugging has traditionally been difficult because developers may see compiler-generated state-machine details while stepping through code.

Runtime Async improves this experience.

Microsoft documents improvements including breakpoint binding inside Runtime Async methods and debugger stepping across await boundaries without exposing compiler-generated infrastructure.

This is not directly a throughput improvement, but it can have a significant developer-productivity impact.

ReadyToRun and NativeAOT

Runtime Async is not limited to JIT-only applications.

Microsoft documents support for both ReadyToRun and NativeAOT scenarios.

This is important because deployment models can change runtime behavior.

A benchmark should therefore identify which execution mode it is measuring:

JIT
 |
 +-- .NET 10
 +-- .NET 11

and, separately:

ReadyToRun
 |
 +-- .NET 10
 +-- .NET 11

and where applicable:

NativeAOT
 |
 +-- .NET 10
 +-- .NET 11

Do not compare a JIT build against a NativeAOT build and attribute the entire difference to Runtime Async.

That would combine multiple variables.

Runtime Async and ASP.NET Core

ASP.NET Core is an especially interesting environment because web applications commonly contain asynchronous pipelines:

HTTP Request
     |
     v
Middleware
     |
     v
Controller / Endpoint
     |
     v
Database
     |
     v
External API
     |
     v
HTTP Response

A request may encounter multiple asynchronous operations.

For example:

app.MapGet(
    "/customers/{id}",
    async (
        int id,
        CustomerRepository repository,
        CancellationToken cancellationToken) =>
    {
        var customer =
            await repository.GetAsync(
                id,
                cancellationToken);

        return Results.Ok(customer);
    });

A benchmark for a real service should measure the complete request pipeline rather than just the await statement.

Useful measurements include:

  • Requests per second

  • p50 latency

  • p95 latency

  • p99 latency

  • Allocated bytes per request

  • CPU utilization

  • GC activity

This provides a much more useful picture than a microbenchmark alone.

What Runtime Async Does Not Change

It is equally important to understand what the feature does not automatically solve.

Runtime Async does not make slow database queries fast.

This remains true:

await database.ExecuteLongRunningQueryAsync();

If the database takes 500 milliseconds to respond, reducing async infrastructure overhead does not turn the query into a 1-millisecond operation.

Similarly, it does not automatically improve:

  • Network latency

  • Database execution plans

  • Slow APIs

  • CPU-heavy algorithms

  • Excessive serialization

  • Poor connection pooling

  • Lock contention

The async runtime is only one component of the application's performance profile.

A Better Benchmark Matrix

For serious evaluation, build a matrix such as:

Test.NET Baseline.NET 11 Runtime Async
Synchronous completionMeasureMeasure
One suspensionMeasureMeasure
Multiple awaitsMeasureMeasure
TaskMeasureMeasure
ValueTaskMeasureMeasure
AsyncLocal presentMeasureMeasure
AsyncLocal absentMeasureMeasure
ASP.NET Core requestMeasureMeasure
ReadyToRunMeasureMeasure
NativeAOTMeasureMeasure

This allows you to identify where the feature actually changes application behavior.

Avoiding False Conclusions

Mistaking Microbenchmark Results for Application Performance

A five-line benchmark may show a measurable difference while the production application sees no meaningful change.

Always validate important findings with a representative workload.

Measuring Only Mean Latency

Averages can hide tail latency.

For web applications, p95 and p99 latency can be more useful than a single mean value.

Ignoring Allocations

An operation that is slightly faster but creates substantially more garbage may not be an improvement for a high-throughput service.

Changing Multiple Variables

When comparing runtimes, keep these constant:

  • Hardware

  • Operating system

  • CPU architecture

  • Application code

  • Dependencies

  • Benchmark configuration

  • Input data

Change the runtime first.

Benchmarking Preview Software as Final Behavior

Runtime Async is still a preview feature.

Results from one preview build should not be treated as a permanent performance guarantee. Microsoft continues to evolve the implementation during the .NET 11 development cycle.

Production Considerations

Before enabling Runtime Async broadly, test your actual application.

A reasonable evaluation process is:

  1. Create a representative performance test.

  2. Establish a baseline on the current runtime.

  3. Move the same workload to .NET 11.

  4. Enable Runtime Async.

  5. Measure latency and throughput.

  6. Measure allocations and GC activity.

  7. Test diagnostic and debugging workflows.

  8. Repeat under realistic concurrency.

  9. Compare results.

  10. Roll out gradually if the results justify adoption.

For a large service, include production-like traffic patterns rather than relying solely on synthetic benchmarks.

Troubleshooting Unexpected Results

No Measurable Performance Improvement

That is not necessarily a problem.

If most application time is spent in database calls, network operations, serialization, or business logic, async infrastructure overhead may represent only a small portion of total request time.

Performance Gets Worse

First verify that the benchmark is stable.

Check:

  • CPU frequency behavior

  • Background processes

  • GC activity

  • Thread scheduling

  • Benchmark warmup

  • Runtime configuration

  • Debug versus Release

  • JIT versus ReadyToRun

Then profile the workload before concluding that Runtime Async is responsible.

Stack Traces Look Different

That is expected in live execution.

Runtime Async intentionally changes the representation of asynchronous execution to make the actual methods more visible.

Existing Libraries Behave Differently

Do not assume that every dependency was compiled under the same Runtime Async configuration.

Test the complete application rather than only a standalone project.

Best Practices

Benchmark Before Optimizing

Measure first.

Do not enable a preview performance feature simply because it sounds faster.

Separate Runtime and Application Costs

Measure both:

Async infrastructure
+
Database
+
Network
+
Serialization
+
Business logic

This prevents the runtime from receiving credit or blame for unrelated bottlenecks.

Test the Synchronous Fast Path

Many APIs complete synchronously under certain conditions.

This is one of the areas where Runtime Async's JIT improvements are particularly relevant.

Test Real Suspension

A benchmark that only uses already-completed tasks does not represent workloads where asynchronous operations actually suspend.

Measure Memory

Use allocation diagnostics alongside timing.

Record the Runtime Version

For preview features, record the exact SDK and runtime build used for the experiment.

Frequently Asked Questions

Does .NET 11 make all async code faster?

No.

.NET 11 introduces Runtime Async and several related runtime optimizations, but the benefit depends on the workload. Applications dominated by database, network, or CPU costs may see little overall change.

Does Runtime Async eliminate async state machines?

It changes the execution model for methods using Runtime Async so that suspension and resumption are managed by the runtime rather than relying entirely on compiler-generated state-machine infrastructure.

Is Runtime Async production-ready?

Runtime Async is currently documented as a preview feature. Teams should evaluate it carefully before relying on it in production.

Does Runtime Async improve debugging?

Yes, Microsoft documents cleaner live stack traces and improved breakpoint and stepping behavior for Runtime Async methods.

Should I replace Task with ValueTask?

Not simply because of Runtime Async.

Task and ValueTask have different semantics and trade-offs. Choose based on the API's actual completion behavior and measured allocation/performance characteristics.

Conclusion

.NET 11's Runtime Async is more than a small optimization to the existing async/await implementation.

It changes how asynchronous suspension and continuation are represented and managed by the runtime. Microsoft documents improvements in runtime integration, JIT support, continuation handling, code generation, debugging, and live stack traces.

The performance question, however, should be answered experimentally.

A useful evaluation does not ask:

"Is .NET 11 async faster?"

It asks:

Which async workloads change?
How much do they change?
Where does the improvement come from?
Does it matter to the application?

The most reliable benchmark strategy is therefore:

Baseline
   |
   v
Synchronous Fast Path
   |
   v
Real Suspension
   |
   v
Multiple Awaits
   |
   v
ExecutionContext
   |
   v
Task / ValueTask
   |
   v
ASP.NET Core Workload
   |
   v
Measure Latency + Allocations

.NET 11 gives developers a meaningful new runtime implementation to evaluate, but the correct production decision should come from measurements on the application's real workload—not from a generic benchmark or the assumption that a newer runtime automatically produces faster code.