← Playground bench/HTTP3_RESULTS.md
raw

NURL HTTP/3-server peer-comparison

Generated 2026-10-22T06:07:03Z by bench/run_http3.sh. Do not edit by hand — the next run overwrites it.

The HTTP/3 companion of HTTP_RESULTS.md (HTTP/1.1) and HTTP2_RESULTS.md (HTTP/2), which stay as they are. Each implementation terminates QUIC (RFC 9000/9001/9002) on a UDP socket, speaks HTTP/3 (RFC 9114 + QPACK, RFC 9204) and answers every request on every stream with the same 14-byte Hello, World!\n body (text/plain), over a self-signed EC (P-256) certificate the generator accepts without verification.

The NURL server is bench/http_server.nu unchanged from the HTTP/1.1 and HTTP/2 benchmarks: the packages/http HttpApp facade's TLS listener also binds its port over UDP and serves HTTP/3 there through the same routes; the QUIC transport, the TLS 1.3 handshake inside it, QPACK and the HTTP/3 framing are all pure NURL. The Rust peer is quinn + h3 (h3-quinn) on tokio with rustls — the stack behind most Rust HTTP/3 servers.

Cells are C x M: C client connections, each with M concurrent streams (h2load --h3 -c C -m M), so C x M requests are in flight. The same closed-loop caveat as the HTTP/2 report applies: at high in-flight counts read req/s, not the latency columns; cells whose effective concurrency (req/s x mean-latency) falls well short of C x M are marked ‡ and left un-bold.

Environment

ItemValue
HostGitHub Actions ubuntu-latest runner
KernelLinux 7.0.0-1017-azure x86_64
CPUAMD EPYC 9V45 96-Core Processor (4 logical cores)
Memory16367520 KiB
Commit4cc2a80a00753b0ab965b79977e142b17d2cf2e9
CI runhttps://github.com/nurl-lang/nurl/actions/runs/39731345494
NURLv0.76.0-32-g4cc2a80a
Rustrustc 1.99.1 (58cf8f9c3 2026-10-13)
Load generatorh2load nghttp2/1.70.0 (docker nghttp2-h3)
SettingValue
Throughput/latencymedian of 3 x 10 s closed-loop runs
Cells (C x M)1x1 , 1x10 , 1x100 , 10x1 , 10x10 , 50x1 , 50x10
Connection setup100 fresh connections, one request each, median of 3 runs
TLS certself-signed EC P-256, CN=localhost, ALPN h3

1. HTTP/3 over QUIC

Server1 x 11 x 101 x 10010 x 110 x 1050 x 150 x 10
req/sNURL25 089141 088261 06068 802215 71866 656213 784
Rust4450 40190 04940464 7001 92523 591
p50 (ms)NURL0.040.070.28‡0.140.450.692.24
Rust26.350.180.86‡26.200.2026.2926.16
p99 (ms)NURL0.050.090.41‡0.210.701.153.87
Rust26.630.511.60‡27.7626.0527.2327.65

‡ closed-loop starved (NURL 1x100: ~75.7 in flight; Rust 1x100: ~77.4 in flight).

2. NURL, same server and listener: HTTP/3 vs HTTP/2 (M = 1)

The same binary and the same host:port — QUIC over UDP for HTTP/3, TLS over TCP with ALPN h2 for HTTP/2 — driven by the same h2load. The gap is the transports' own cost (packet protection and per-packet framing for QUIC, records for TLS/TCP) with everything else held equal. HTTP/1.1 on this listener is in HTTP_RESULTS.md (oha): h2load's HTTP/1.1 mode paces itself and would misreport it.

CHTTP/3 req/sHTTP/2 req/sHTTP/3 / HTTP/2HTTP/3 p50 (ms)HTTP/2 p50 (ms)
125 0891 00025.09x0.040.04
1068 80210 0006.88x0.140.12
5066 65644 5601.50x0.690.57

3. Connection setup rate

100 clients each open a fresh connection, make one request and close: a QUIC handshake (Initial + Handshake, one round trip, keys derived on both ends) for HTTP/3, a TLS 1.3 handshake over a new TCP connection for HTTP/2. Connections per second, median of the runs.

ServerProtocolconn/s
NURLHTTP/3 (QUIC)1 665
NURLHTTP/2 (TLS+TCP)1 466
RustHTTP/3 (QUIC)1 620

(Best per column in bold; latency winners are chosen only among non-starved cells. ‡ = closed-loop starved. n/a = tool absent; FAIL = the server did not complete that cell.)

Notes

Planned rigor

  1. Realistic bodies (1 KB / 16 KB / 1 MB) where flow control, pacing and datagram size matter.
  2. Lossy paths (tc netem loss/delay) — the QUIC recovery machinery is not exercised on loopback.
  3. Batched UDP I/O (recvmmsg / GSO) and core isolation, as in the torture harness.