← Playground bench/HTTP2_RESULTS.md
raw

NURL HTTP/2-server peer-comparison

Generated 2026-09-14T03:51:06Z 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 6.17.0-1022-azure x86_64
CPUAMD EPYC 7763 64-Core Processor (4 logical cores)
Memory16373452 KiB
Commit079fb775b6a998d2e5b1dbe9fb09d8e1cc213768
CI runhttps://github.com/nurl-lang/nurl/actions/runs/34803799886
NURLv0.65.0-10-g079fb775
Rustrustc 1.98.1 (48a229cea 2026-09-01)
Nodev22.23.2
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/sNURL15 61961 009128 12550 067138 08575 617165 450
Rust10 75053 055115 06958 143135 29082 358147 523
Node7 42134 01265 30518 92052 25518 76150 401
p50 (ms)NURL0.060.160.760.180.660.612.75
Rust0.080.180.780.150.650.603.31
Node0.120.271.390.421.542.218.68
p99 (ms)NURL0.100.221.130.511.862.556.63
Rust0.120.281.130.401.461.166.09
Node0.170.373.401.294.784.3219.20

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)
115 61923 9880.65x0.060.03
1050 06772 6660.69x0.180.12
5075 617106 9460.71x0.610.43

2. HTTP/2 over TLS (ALPN h2)

Server1 x 11 x 101 x 10010 x 110 x 1050 x 150 x 10
req/sNURL12 61245 91987 00239 473102 89654 497120 288
Rust9 62848 251115 74545 483118 92267 804129 621
Node6 65232 10563 87015 41049 75214 89745 938
p50 (ms)NURL0.070.211.130.220.890.814.14
Rust0.090.200.840.200.750.723.80
Node0.130.281.430.551.792.829.74
p99 (ms)NURL0.120.291.580.652.543.168.80
Rust0.130.311.050.471.631.436.84
Node0.190.393.721.034.134.6322.59

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)
112 61216 5270.76x0.070.05
1039 47354 3170.73x0.220.16
5054 49779 2860.69x0.810.57

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