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:
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.
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:
| Test | HTTP/2 | HTTP/3 |
|---|
| Cold connection | Yes | Yes |
| Warm connection | Yes | Yes |
| Concurrency 1 | Yes | Yes |
| Concurrency 50 | Yes | Yes |
| Concurrency 100 | Yes | Yes |
| Small JSON | Yes | Yes |
| Large JSON | Yes | Yes |
| POST request | Yes | Yes |
| Database request | Yes | Yes |
| Packet loss | Yes | Yes |
| Added latency | Yes | Yes |
| CPU | Measure | Measure |
| Memory | Measure | Measure |
This matrix produces considerably more useful information than a single curl request.
Example Results Format
When publishing benchmark results, use measured values:
| Scenario | Protocol | p50 | p95 | p99 | RPS | CPU |
|---|
| Small JSON | HTTP/2 | Measure | Measure | Measure | Measure | Measure |
| Small JSON | HTTP/3 | Measure | Measure | Measure | Measure | Measure |
| DB API | HTTP/2 | Measure | Measure | Measure | Measure | Measure |
| DB API | HTTP/3 | Measure | Measure | Measure | Measure | Measure |
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
Benchmark HTTP/2 and HTTP/3 using the same application.
Define exactly what "startup" or "latency" means before testing.
Measure cold and warm connections separately.
Verify that the client actually uses HTTP/3.
Test realistic concurrency levels.
Measure p50, p95, and p99 latency.
Measure throughput and error rate.
Test under controlled packet loss and latency.
Include both lightweight and database-backed endpoints.
Measure CPU and memory alongside latency.
Document the OS, .NET version, MsQuic version, hardware, and network.
Keep reverse-proxy behavior separate from Kestrel behavior.
Use HTTPS for HTTP/3 tests.
Do not increase HTTP/3 resource limits without understanding the security impact.
Repeat tests instead of relying on one measurement.
Do not publish unsupported benchmark percentages.
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.