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:
| Mode | Description |
|---|---|
| New connection | Establish connection for each test |
| Reused connection | Send 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:
| Concurrency | HTTP/2 RPS | HTTP/3 RPS |
|---|---|---|
| 1 | Measure | Measure |
| 10 | Measure | Measure |
| 50 | Measure | Measure |
| 100 | Measure | Measure |
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.
Join the conversation! Your thoughts help the community grow.