← Playground bench/HTTP3_RESULTS.md
raw

NURL HTTP/3-server peer-comparison

Generated 2026-09-13T21:04:56Z 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 6.17.0-1022-azure x86_64
CPUAMD EPYC 9V74 80-Core Processor (4 logical cores)
Memory16373452 KiB
Commita7146d7d90a106a97e8b205454fdeb42f572cf4d
CI runhttps://github.com/nurl-lang/nurl/actions/runs/34782329117
NURLv0.65.0-4-ga7146d7d
Rustrustc 1.98.1 (48a229cea 2026-09-01)
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/sNURL13 55164 109109 98327 80193 02827 54192 177
Rust5420 72862 05440232 8181 91924 898
p50 (ms)NURL0.070.140.67‡0.351.061.805.31
Rust26.340.391.12‡26.220.4326.2526.06
p99 (ms)NURL0.100.190.94‡0.491.492.427.52
Rust26.701.292.36‡27.1226.2527.2827.41

‡ closed-loop starved (NURL 1x100: ~77.0 in flight; Rust 1x100: ~72.6 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)
113 5511 00013.55x0.070.06
1027 80110 0002.78x0.350.19
5027 54125 9851.06x1.800.98

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 066
NURLHTTP/2 (TLS+TCP)895
RustHTTP/3 (QUIC)1 014

(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.