← Playground bench/HTTP2_RESULTS.md
raw

NURL HTTP/2-server peer-comparison

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

The HTTP/2 companion of HTTP_RESULTS.md (which stays HTTP/1.1-only). Each implementation accepts a connection, speaks HTTP/2 (RFC 9113 + HPACK, RFC 7541) and answers every request on every stream with the same 14-byte Hello, World!\n body (text/plain). Section 1 is cleartext HTTP/2 with prior knowledge (§3.4 — the PRI * HTTP/2.0 preface, what curl --http2-prior-knowledge, h2load and oha --http2 send); section 2 negotiates h2 over ALPN (§3.3) on a self-signed EC (P-256) certificate, which oha accepts with --insecure.

The NURL server is bench/http_server.nu unchanged from the HTTP/1.1 benchmark: the packages/http HttpApp facade in http_app_async mode serves both protocols on every listener — ALPN decides over TLS, the connection preface decides on cleartext. The Rust peer drives hyper's http2 connection builder on tokio (rustls with ALPN h2 for TLS); the Node peer is the built-in node:http2 module (allowHTTP1: false for TLS).

Cells are C x P: C connections, each carrying P concurrent streams (oha --http2 -c C -p P), so C x P requests are in flight. 1 x 100 is one connection multiplexing a hundred streams — HTTP/2's own axis, which HTTP/1.1 has no equivalent for; 50 x 1 is fifty connections with one stream each, the closest thing to the HTTP/1.1 C = 50 cell.

Read the throughput columns, not the latency columns, at high in-flight counts. These are closed-loop measurements: oha fires the next request on a stream the instant the previous one returns. If a server's in-flight work saturates below C x P, the extra requests queue inside oha and never reach the server, so req/s is the server's true saturation throughput but the latency percentiles describe only the requests in flight. Such cells are marked ‡ and left un-bold. The effective in-flight count is req/s x mean-latency (Little's law).

Environment

ItemValue
HostGitHub Actions ubuntu-latest runner
KernelLinux 7.0.0-1017-azure x86_64
CPUAMD EPYC 9V74 80-Core Processor (4 logical cores)
Memory16367524 KiB
Commit4cc2a80a00753b0ab965b79977e142b17d2cf2e9
CI runhttps://github.com/nurl-lang/nurl/actions/runs/39731307771
NURLv0.76.0-32-g4cc2a80a
Rustrustc 1.99.1 (58cf8f9c3 2026-10-13)
Nodev22.23.3
Load generatoroha 1.8.0
SettingValue
Throughput/latencymedian of 3 x 10 s closed-loop runs
Cells (C x P)1x1 , 1x10 , 1x100 , 10x1 , 10x10 , 50x1 , 50x10
TLS certself-signed EC P-256, CN=localhost, ALPN h2

1. Cleartext HTTP/2 (h2c, prior knowledge)

Server1 x 11 x 101 x 10010 x 110 x 1050 x 150 x 10
req/sNURL18 51270 215157 82168 801174 20386 954201 519
Rust18 81571 606124 47970 008154 49584 863163 056
Node11 60541 41769 21128 92156 64427 58955 741
p50 (ms)NURL0.050.140.610.120.520.522.19
Rust0.050.130.710.130.570.583.00
Node0.070.211.270.261.611.469.00
p99 (ms)NURL0.080.180.900.431.493.055.64
Rust0.080.211.000.311.281.135.65
Node0.140.323.370.924.402.8518.87

NURL, same server and listener: HTTP/2 (P = 1) vs HTTP/1.1

The same binary, the same port, oha with and without --http2. The gap is the protocol's own cost — framing, HPACK, flow-control bookkeeping — with everything else held equal.

CHTTP/2 req/sHTTP/1.1 req/sHTTP/2 / HTTP/1.1HTTP/2 p50 (ms)HTTP/1.1 p50 (ms)
118 51223 4600.79x0.050.04
1068 80196 1380.72x0.120.09
5086 954118 1400.74x0.520.41

2. HTTP/2 over TLS (ALPN h2)

Server1 x 11 x 101 x 10010 x 110 x 1050 x 150 x 10
req/sNURL16 40258 841128 56759 709148 12377 055170 748
Rust17 15667 294121 96357 370136 66874 426146 401
Node10 15737 00066 03821 64856 04622 18348 997
p50 (ms)NURL0.050.160.750.150.610.572.59
Rust0.050.140.760.160.660.683.35
Node0.080.241.330.371.551.8910.11
p99 (ms)NURL0.090.221.080.471.703.546.68
Rust0.100.210.900.361.431.236.06
Node0.170.353.810.733.863.5921.33

NURL, same server and listener: HTTP/2 (P = 1) vs HTTP/1.1

The same binary, the same port, oha with and without --http2. The gap is the protocol's own cost — framing, HPACK, flow-control bookkeeping — with everything else held equal.

CHTTP/2 req/sHTTP/1.1 req/sHTTP/2 / HTTP/1.1HTTP/2 p50 (ms)HTTP/1.1 p50 (ms)
116 40221 6700.76x0.050.04
1059 70986 3100.69x0.150.11
5077 05593 5590.82x0.570.52

(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. Open-loop latency. A fixed-rate generator (oha -q --latency-correction --http2) at 50/80/95 % of each server's measured throughput, reporting p50/p99/p99.9 — the bench/http_torture treatment, for HTTP/2.
  2. Realistic bodies. 1 KB / 16 KB / 1 MB responses, where DATA framing, flow-control windows and the per-stream WINDOW_UPDATE traffic start to matter; a 14-byte body measures HEADERS + HPACK.
  3. Core isolation (server and generator on disjoint cores) and CPU-time per request, as in the torture harness.