ASP.NET Core  

Benchmarking ASP.NET Core HTTP/3 Under Real API Load

HTTP/3 is no longer just a protocol specification to watch.

ASP.NET Core Kestrel supports HTTP/3 through QUIC, and Microsoft continues to improve its implementation. HTTP/3 uses QUIC instead of TCP, provides independent streams, and can reduce connection-establishment latency compared with older HTTP versions.

For API developers, however, the important question is not:

Is HTTP/3 faster than HTTP/2?

The more useful question is:

How does HTTP/3 behave under the actual workload of an ASP.NET Core API?

That requires more than sending a few requests with a browser.

A meaningful benchmark should examine:

  • Connection establishment

  • First-request latency

  • Warm-request latency

  • Throughput

  • Concurrent requests

  • Packet loss

  • Request payload size

  • Response payload size

  • CPU usage

  • Memory usage

  • Connection reuse

This article explains how to build such a benchmark without inventing performance numbers that may not apply to your infrastructure.

HTTP/2 vs HTTP/3

The biggest architectural difference is the transport.

HTTP/2 uses TCP.

HTTP/3 uses QUIC.

A simplified model is:

HTTP/2
Client
  |
  v
TCP
  |
  v
TLS
  |
  v
HTTP/2
  |
  v
ASP.NET Core

HTTP/3
Client
  |
  v
QUIC
  |
  v
HTTP/3
  |
  v
ASP.NET Core

QUIC provides encrypted transport and independent streams. This can reduce the impact of packet loss compared with TCP-based multiplexing, where packet loss can cause head-of-line blocking across streams.

Why Benchmark HTTP/3?

HTTP/3 can be particularly interesting for applications where network conditions matter.

Examples include:

  • Mobile clients

  • Public APIs

  • High-latency networks

  • Lossy networks

  • APIs with many concurrent requests

  • Applications establishing many new connections

However, HTTP/3 does not automatically make every API faster.

For a request dominated by database execution:

HTTP Request
     |
     v
ASP.NET Core
     |
     v
Database
     |
     v
500 ms query

a transport-level improvement of a few milliseconds may have little effect on total latency.

For a lightweight endpoint:

HTTP Request
     |
     v
ASP.NET Core
     |
     v
200 OK

network and connection behavior can represent a much larger portion of the total response time.

That is why benchmarking the actual workload matters.

HTTP/3 Support in Kestrel

HTTP/3 is not enabled automatically.

A Kestrel endpoint can be configured to support HTTP/1.1, HTTP/2, and HTTP/3 together:

using Microsoft.AspNetCore.Server.Kestrel.Core;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenAnyIP(5001, listenOptions =>
    {
        listenOptions.Protocols =
            HttpProtocols.Http1AndHttp2AndHttp3;

        listenOptions.UseHttps();
    });
});

var app = builder.Build();

app.MapGet(
    "/api/health",
    () => Results.Ok(new
    {
        Status = "Healthy"
    }));

app.Run();

HTTP/3 requires HTTPS, and Microsoft recommends supporting HTTP/1.1 and HTTP/2 alongside HTTP/3 because infrastructure and clients may not always support HTTP/3.

HTTP/3 Uses QUIC

HTTP/3 is built on QUIC.

A simplified request path is:

Application
    |
    v
HTTP/3
    |
    v
QUIC
    |
    v
UDP

This differs from HTTP/2:

Application
    |
    v
HTTP/2
    |
    v
TLS
    |
    v
TCP

QUIC provides multiplexed streams without relying on TCP's single ordered byte stream.

This is one reason HTTP/3 can behave differently when packets are lost.

.NET 11 First-Request Processing

One particularly relevant change for a benchmark is the HTTP/3 first-request path in .NET 11.

Microsoft documents that Kestrel can process HTTP/3 requests without first waiting for the QUIC control stream and initial SETTINGS frame. Earlier ASP.NET Core versions waited for those elements before processing request streams. The change is intended to reduce latency for the first request on a new HTTP/3 connection.

That means a useful benchmark should distinguish:

New Connection
    |
    v
First Request

from:

Existing Connection
    |
    v
Subsequent Request

Combining both into a single average can hide important behavior.

Define the Benchmark

Before writing the test, define exactly what is being measured.

For example:

Benchmark:
ASP.NET Core API

Protocols:
HTTP/2 vs HTTP/3

Endpoint:
GET /api/products

Payload:
JSON

Concurrency:
1, 10, 50, 100 clients

Metrics:
Latency
Throughput
CPU
Memory

Keep the application and infrastructure identical between the protocol tests.

Only the protocol should change.

Create a Lightweight API

Start with a simple endpoint:

app.MapGet(
    "/api/products",
    () =>
    {
        return Results.Ok(new[]
        {
            new { Id = 1, Name = "Laptop" },
            new { Id = 2, Name = "Monitor" },
            new { Id = 3, Name = "Keyboard" }
        });
    });

This is useful for measuring transport and framework overhead.

It should not be considered representative of every production API.

For a more realistic experiment, replace it with an endpoint that performs the actual work of your application.

Add a Larger Response

Small responses may not expose meaningful differences in every environment.

You can create a larger payload:

app.MapGet(
    "/api/products-large",
    () =>
    {
        var products = Enumerable
            .Range(1, 1000)
            .Select(id => new
            {
                Id = id,
                Name = $"Product {id}",
                Price = id * 10.5
            });

        return Results.Ok(products);
    });

Now the benchmark can compare:

Small Response
vs
Large Response

This helps determine whether response size changes the observed behavior.

Test a Database-Backed Endpoint

A production benchmark should eventually include real application work.

For example:

app.MapGet(
    "/api/orders/{id:int}",
    async (
        int id,
        OrderDbContext db,
        CancellationToken cancellationToken) =>
    {
        var order =
            await db.Orders
                .AsNoTracking()
                .FirstOrDefaultAsync(
                    x => x.Id == id,
                    cancellationToken);

        return order is null
            ? Results.NotFound()
            : Results.Ok(order);
    });

Now the request path is:

Client
  |
  v
HTTP/3
  |
  v
Kestrel
  |
  v
ASP.NET Core
  |
  v
EF Core
  |
  v
Database

The transport is only one component of total latency.

Test Three Workload Types

A useful benchmark should contain at least three scenarios.

Scenario 1: Lightweight API

GET /api/health

Purpose:

Measure framework and transport behavior.

Scenario 2: JSON API

GET /api/products

Purpose:

Measure request and response processing.

Scenario 3: Database API

GET /api/orders/1001

Purpose:

Measure a more realistic application workload.

This prevents conclusions based only on an artificially simple endpoint.

HTTP/3 Client Configuration

The .NET HttpClient supports HTTP/3.

You can explicitly request HTTP/3:

using System.Net;

var client = new HttpClient();

var request =
    new HttpRequestMessage(
        HttpMethod.Get,
        "https://localhost:5001/api/health");

request.Version = HttpVersion.Version30;

var response =
    await client.SendAsync(request);

Alternatively, version negotiation can be requested:

request.VersionPolicy =
    HttpVersionPolicy.RequestVersionOrHigher;

Microsoft documents both approaches for HTTP/3 requests from .NET HttpClient.

For controlled benchmarking, explicitly requesting HTTP/3 is generally useful because it makes the protocol under test unambiguous.

Do Not Benchmark a Fallback

A common benchmarking mistake is assuming that:

RequestVersionOrHigher

means:

HTTP/3

It does not necessarily mean the request actually used HTTP/3.

The client may negotiate another supported protocol depending on the environment.

Your benchmark must verify the protocol being used.

Otherwise, you may believe you are testing HTTP/3 while actually measuring HTTP/2.

HTTP/3 Requires Platform Support

Kestrel's HTTP/3 implementation depends on MsQuic.

If the platform does not satisfy the HTTP/3 requirements, Kestrel can disable HTTP/3 and fall back to other protocols.

Therefore, record:

Operating System
.NET Version
ASP.NET Core Version
MsQuic Version
CPU Architecture
Container Image
Kernel Version

as part of the benchmark environment.

Use HTTPS

HTTP/3 requires TLS.

For local development, configure a certificate appropriately.

For production-like testing, use a valid certificate and the same TLS configuration you expect in deployment.

Do not compare:

HTTP/2 + production TLS

against:

HTTP/3 + simplified local setup

and treat the results as directly representative of production.

Warm vs Cold Connections

This is one of the most important parts of the benchmark.

Measure both:

Cold Connection
Client
  |
  v
New QUIC Connection
  |
  v
First Request

and:

Warm Connection
Existing QUIC Connection
  |
  v
Request

The first-request path can behave differently because connection establishment and protocol initialization occur before application processing.

Why First Request Matters

For applications that maintain long-lived connections:

Connect
   |
   v
Request 1
   |
   v
Request 2
   |
   v
Request 3
   |
   v
Request 100

the connection setup cost is amortized.

For short-lived clients:

Connect
   |
   v
Request
   |
   v
Disconnect

connection establishment can represent a much larger fraction of the total latency.

Your workload determines which measurement is more important.

Benchmark Connection Reuse

Test at least two modes:

ModeDescription
New connectionEstablish connection for each test
Reused connectionSend multiple requests over the same connection

This helps answer:

Is the observed performance difference caused primarily by connection establishment or request processing?

Test Concurrent Requests

A single request does not represent real API traffic.

Test concurrency levels such as:

1
10
50
100
500

The exact levels should match the expected workload and available infrastructure.

For each level, collect:

Requests/sec
Median latency
p95 latency
p99 latency
Errors
CPU
Memory

Do not choose concurrency levels simply because they produce favorable numbers.

Throughput

Throughput measures how much work the API completes over a period.

For example:

Requests
   |
   v
Requests per second

A simplified result table might look like:

ConcurrencyHTTP/2 RPSHTTP/3 RPS
1MeasureMeasure
10MeasureMeasure
50MeasureMeasure
100MeasureMeasure

The actual values should come from the benchmark environment.

Latency Distribution

Average latency is not enough.

Suppose:

Average = 30 ms

That does not tell you whether:

p95 = 35 ms

or:

p95 = 500 ms

For production APIs, measure:

p50
p95
p99

where appropriate.

Tail latency becomes particularly important under high concurrency.

Packet Loss Testing

HTTP/3's QUIC transport can behave differently under packet loss because independent streams avoid TCP-level head-of-line blocking. Microsoft identifies this as one of the key HTTP/3 advantages.

A useful experiment is:

0% packet loss
0.1%
0.5%
1%

The exact values should reflect realistic network conditions.

Then compare:

HTTP/2
vs
HTTP/3

under identical network impairment.

This can reveal differences that are invisible on a clean local network.

Add Network Latency

Localhost benchmarks can be misleading.

A local test may look like:

Client
 |
 |  <1 ms
 |
 v
Server

A production client may experience:

Client
 |
 |  50 ms
 |
 v
Server

or more.

For network-sensitive testing, introduce controlled latency.

Measure:

Low latency
Medium latency
High latency

The objective is not to make HTTP/3 look better.

The objective is to determine where its transport characteristics matter.

Do Not Change Multiple Variables at Once

Avoid this comparison:

HTTP/2
Windows
1 CPU
Local database

vs

HTTP/3
Linux
4 CPUs
Remote database

Any difference becomes difficult to interpret.

Instead:

Same Host
Same CPU
Same Memory
Same Application
Same Database
Same Payload
Same Client
Different HTTP Version

This creates a controlled experiment.

Measure CPU

QUIC introduces different transport behavior and therefore should be evaluated for CPU consumption as well as latency.

Record:

Average CPU
Peak CPU
CPU per request
CPU at each concurrency level

A protocol that improves latency but significantly changes CPU consumption may produce a different infrastructure trade-off.

Do not infer CPU efficiency solely from throughput.

Measure it.

Measure Memory

Collect:

Process Memory
Container Memory
Working Set / RSS
GC Metrics

where applicable.

Compare the same workload under both protocols.

Memory behavior can become important when running many API instances.

Test Large Requests

Do not test only GET requests.

Add POST requests:

app.MapPost(
    "/api/orders",
    async (
        CreateOrderRequest request,
        OrderDbContext db,
        CancellationToken cancellationToken) =>
    {
        var order = new Order
        {
            CustomerId = request.CustomerId
        };

        db.Orders.Add(order);

        await db.SaveChangesAsync(
            cancellationToken);

        return Results.Created(
            $"/api/orders/{order.Id}",
            order);
    });

Now measure:

Small request body
Large request body
Small response
Large response

This creates a more complete workload.

Test Streaming Separately

Streaming workloads can behave differently from ordinary request/response APIs.

Examples include:

  • Large file downloads

  • Server-sent data

  • Long-running responses

  • Large uploads

Do not assume that the benchmark result for a small JSON response applies to streaming workloads.

Create a separate test.

HTTP/3 and Head-of-Line Blocking

Suppose multiple requests share a connection:

Stream A
Stream B
Stream C
Stream D

With TCP-based HTTP/2, packet loss can affect the TCP connection and therefore delay delivery across streams.

QUIC provides independent streams, reducing this type of cross-stream blocking.

A packet-loss benchmark should therefore use concurrent requests rather than a single request.

Otherwise, there are no parallel streams to observe.

HTTP/3 Connection Migration

QUIC can support connection migration when a client's network changes.

This can be relevant for mobile clients moving between:

Wi-Fi
   |
   v
Cellular

Microsoft identifies connection migration as a potential HTTP/3 benefit, although the exact behavior depends on the client and server implementation.

This should be treated as a separate resilience experiment rather than mixed into a basic throughput benchmark.

Benchmark Behind a Reverse Proxy

A production ASP.NET Core deployment may look like:

Internet
   |
   v
Load Balancer
   |
   v
Reverse Proxy
   |
   v
Kestrel
   |
   v
ASP.NET Core

In this architecture, the client-facing protocol may terminate at the load balancer.

For example:

Client
  |
  | HTTP/3
  v
Edge
  |
  | HTTP/2
  v
Application

This is not an end-to-end HTTP/3 application path.

Document where HTTP/3 terminates before interpreting the benchmark.

YARP and HTTP/3

If a reverse proxy such as YARP is part of the architecture, test both inbound and outbound protocol behavior separately.

Microsoft documents HTTP/3 support for YARP using Kestrel for inbound connections and HttpClient for outbound connections.

Your benchmark should therefore identify:

Client -> Proxy
Proxy -> Application

as separate network segments.

HTTP/3 Security Considerations

HTTP/3 uses QUIC, and QUIC mandates TLS 1.3.

Microsoft's Kestrel security documentation also identifies resource-exhaustion concerns involving streams and connections as important HTTP/3 security considerations.

A performance test should therefore not ignore:

Connection Limits
Stream Limits
Header Limits
Request Body Limits
Resource Exhaustion

Performance tuning and security configuration must be evaluated together.

Test Header Size

Kestrel exposes HTTP/3-specific limits.

For example, Http3Limits.MaxRequestHeaderFieldSize controls the maximum size of an individual request header field. Microsoft documents a default of 32,768 bytes in the current Kestrel HTTP/3 security guidance.

Do not increase limits simply because a benchmark encounters large headers.

First determine why the request needs them.

Test Stream Concurrency

QUIC transport settings include limits such as the maximum number of concurrent bidirectional streams.

Microsoft documents MaxBidirectionalStreamCount as a QUIC transport setting, with a current documented default of 100.

A high-concurrency benchmark should therefore account for stream limits.

If the test generates more concurrent traffic than the connection can support, queueing behavior may become part of the observed latency.

Use a Dedicated Benchmark Environment

Avoid benchmarking on a development laptop while:

IDE
Browser
Docker
Teams
Background builds

are running.

For reproducible results:

Dedicated Machine
        |
        v
Fixed CPU/Memory
        |
        v
Controlled Network
        |
        v
Repeatable Benchmark

The exact hardware should be documented.

Run Multiple Iterations

A useful benchmark sequence is:

Warm-up
   |
   v
Run 1
Run 2
Run 3
...
Run N

Then calculate:

Median
p95
p99
Throughput
Error Rate

Avoid publishing only the fastest run.

Example Benchmark Matrix

A practical experiment can use:

TestHTTP/2HTTP/3
Cold connectionYesYes
Warm connectionYesYes
Concurrency 1YesYes
Concurrency 50YesYes
Concurrency 100YesYes
Small JSONYesYes
Large JSONYesYes
POST requestYesYes
Database requestYesYes
Packet lossYesYes
Added latencyYesYes
CPUMeasureMeasure
MemoryMeasureMeasure

This matrix produces considerably more useful information than a single curl request.

Example Results Format

When publishing benchmark results, use measured values:

ScenarioProtocolp50p95p99RPSCPU
Small JSONHTTP/2MeasureMeasureMeasureMeasureMeasure
Small JSONHTTP/3MeasureMeasureMeasureMeasureMeasure
DB APIHTTP/2MeasureMeasureMeasureMeasureMeasure
DB APIHTTP/3MeasureMeasureMeasureMeasureMeasure

Do not replace Measure with assumed percentages.

The value of this article is the methodology.

Common Benchmarking Mistakes

Measuring Only Localhost

Localhost removes many network characteristics that make HTTP/3 interesting.

Testing Only One Request

A single request does not expose concurrency or multiplexing behavior.

Ignoring Connection Reuse

Cold and warm connections can have very different characteristics.

Assuming HTTP/3 Was Actually Used

Verify the negotiated protocol.

Changing Infrastructure Between Tests

Use identical infrastructure.

Measuring Only Average Latency

Tail latency can tell a different story.

Ignoring Packet Loss

One of HTTP/3's important transport differences becomes more visible under network impairment.

Testing Only Tiny Responses

Payload size can affect transport behavior.

Ignoring Reverse Proxies

The client-facing protocol may terminate before reaching Kestrel.

Treating HTTP/3 as a Universal Optimization

A database-heavy API may see little benefit if application processing dominates total latency.

Troubleshooting

HTTP/3 Is Not Working

Check:

HTTPS
Kestrel configuration
MsQuic
Operating system
Firewall
UDP connectivity
Client support

Kestrel depends on MsQuic for HTTP/3 functionality and can disable HTTP/3 when platform requirements are not satisfied.

Client Falls Back to HTTP/2

Check the requested HTTP version and version policy.

For a controlled test:

request.Version =
    HttpVersion.Version30;

Also verify that the server endpoint actually supports HTTP/3.

Browser Test Fails on Localhost

Browsers do not support HTTP/3 using Kestrel's self-signed development certificate in the same way as a trusted production certificate.

For loopback testing, Microsoft recommends using HttpClient with HTTP/3-specific configuration.

HTTP/3 Works Locally but Not in Production

Check the network path:

Client
  |
  v
Firewall
  |
  v
Load Balancer
  |
  v
Reverse Proxy
  |
  v
Kestrel

UDP connectivity and HTTP/3 support must exist across the relevant infrastructure.

HTTP/3 Is Slower in Your Benchmark

Do not immediately conclude that HTTP/3 is unsuitable.

First determine whether the workload is:

CPU-bound
Database-bound
Network-bound
Connection-bound

Then inspect cold-start, warm-connection, packet-loss, and concurrency results independently.

Best Practices

  1. Benchmark HTTP/2 and HTTP/3 using the same application.

  2. Define exactly what "startup" or "latency" means before testing.

  3. Measure cold and warm connections separately.

  4. Verify that the client actually uses HTTP/3.

  5. Test realistic concurrency levels.

  6. Measure p50, p95, and p99 latency.

  7. Measure throughput and error rate.

  8. Test under controlled packet loss and latency.

  9. Include both lightweight and database-backed endpoints.

  10. Measure CPU and memory alongside latency.

  11. Document the OS, .NET version, MsQuic version, hardware, and network.

  12. Keep reverse-proxy behavior separate from Kestrel behavior.

  13. Use HTTPS for HTTP/3 tests.

  14. Do not increase HTTP/3 resource limits without understanding the security impact.

  15. Repeat tests instead of relying on one measurement.

  16. Do not publish unsupported benchmark percentages.

  17. Evaluate HTTP/3 based on the actual workload rather than protocol-level assumptions.

Frequently Asked Questions

Is HTTP/3 faster than HTTP/2 for ASP.NET Core APIs?

It can provide advantages in connection establishment, packet-loss scenarios, and multiplexed workloads, but there is no universal performance improvement for every API. The application's processing time, network conditions, and connection behavior all affect the result.

Does ASP.NET Core Kestrel support HTTP/3?

Yes. Kestrel supports HTTP/3 through QUIC, with MsQuic providing the QUIC implementation.

Does HTTP/3 require HTTPS?

Yes. HTTP/3 uses QUIC, which requires TLS 1.3. Kestrel's HTTP/3 endpoint therefore requires HTTPS.

Should an ASP.NET Core application support only HTTP/3?

Usually not.

Microsoft recommends configuring HTTP/3 alongside HTTP/1.1 and HTTP/2 because not every client, proxy, router, or network path supports HTTP/3.

How should HTTP/3 performance be measured?

Measure multiple dimensions:

Cold connection
Warm connection
Latency
p95/p99
Throughput
Concurrency
Packet loss
CPU
Memory

This gives a much more reliable picture than a single request-time measurement.

Does HTTP/3 eliminate all latency problems?

No.

If an API spends most of its time waiting for a database or external service, transport-level improvements may have limited impact on end-to-end latency.

Why is the first HTTP/3 request important?

The first request on a new connection includes connection establishment and protocol initialization. .NET 11 also includes Kestrel changes that allow earlier HTTP/3 request processing without waiting for the initial control stream and SETTINGS frame, specifically targeting first-request latency.

Can HTTP/3 improve performance under packet loss?

It can.

QUIC provides independent streams, avoiding the TCP-level head-of-line blocking behavior that can affect multiple HTTP/2 streams when packets are lost.

Does HTTP/3 always use less CPU?

Not necessarily.

CPU behavior depends on the application, concurrency, cryptographic processing, network conditions, and implementation. CPU must be measured as part of the benchmark.

Conclusion

HTTP/3 changes the transport layer used by ASP.NET Core applications.

The simplified architecture changes from:

HTTP/2
   |
   v
TCP

to:

HTTP/3
   |
   v
QUIC

That change provides important capabilities, including independent streams and different connection-establishment behavior. Kestrel also has .NET 11 improvements for earlier processing of HTTP/3 requests on new connections.

But protocol improvements do not automatically translate into a fixed percentage performance gain for every API.

A useful benchmark should instead isolate:

Connection Cost
       +
Request Processing
       +
Network Conditions
       +
Application Work
       +
Infrastructure

and measure each component where practical.

The strongest comparison is therefore not:

HTTP/2 = X ms
HTTP/3 = Y ms

It is:

                  HTTP/2       HTTP/3
Cold Connection   Measure      Measure
Warm Request      Measure      Measure
p95 Latency       Measure      Measure
Throughput        Measure      Measure
Packet Loss       Measure      Measure
CPU               Measure      Measure
Memory            Measure      Measure

This methodology allows developers to determine whether HTTP/3 provides a meaningful advantage for their specific ASP.NET Core workload.

The most important lesson is:

Do not benchmark HTTP/3 as a protocol feature in isolation. Benchmark it as part of the complete API workload, under the network conditions and concurrency levels your application actually expects.