Benchmarks
Instructions per request (callgrind) and nginx comparison — 2026-10-09
The VM’s throughput noise (CV 5–10 %) hides changes of a few percent, so user-space
work per request was measured with callgrind: the server runs under
valgrind --tool=callgrind, 500 warm-up requests, counters zeroed, 4000 requests
(oha -c 4), dump. Only user-space instructions are counted (no kernel). rep stosb
iterations count as instructions, so large memsets look more expensive than they are.
Both experimental settings on (http1_server, upstream_client); hello.txt (static
small file) and /api/x (proxy to the bench upstream).
| build | static | proxy |
|---|---|---|
| h1fix (audit-fixed baseline) | 27 906 | 48 104 |
| opt3: fewer allocations | 27 403 | 45 091 |
| opt5: one lazily moved head timer per connection | 26 937 | 44 900 |
| opt6: header slots and positions not zeroed per message | 22 495 | 35 697 |
| opt7: lookups only for fields the request carries | 22 049 | 34 440 |
nginx 1.31 (1 worker, master_process off) | 14 612 | 30 148 |
opt6 removes two zeroed 4 KiB arrays per parsed head (request and upstream response);
much of the drop is rep stosb counting, the real saving is a few hundred cycles and
8 KiB less L1 traffic per head.
Where the static request’s 22k instructions go (opt6): http crate (HeaderMap insert,
hash, lookups; header name/value validation) 3.7k, scalws_h1 2.4k, pipeline 2.3k,
scalws_static 2.1k, libc (memcpy: future/struct moves) 1.9k, mimalloc 1.9k,
tokio 1.5k, httparse 1.1k. Proxied requests read the clock ~17 times (tokio’s park
loop ~5, the rest in the pipeline, own client and pool).
nginx comparison (opt6, MODE=quick RUNS=3, results: [private host path])
| scenario | scalws rps | nginx 1.24 | nginx 1.31 | scalws cpu µs/req | nginx 1.31 cpu µs/req |
|---|---|---|---|---|---|
| static-small | 95 031 | 98 688 | 98 410 | 34.3 | 31.9 |
| static-1m | 8 428 | 8 750 | 8 794 | 304 | 323 |
| respond | 94 627 | 97 185 | 100 253 | 23.2 | 23.8 |
| proxy | 78 714 (CV 9.7 %) | 88 396 | 92 196 | 43.2 | 38.6 |
| cache-hit | 95 348 | 97 122 | 93 047 | 24.8 | 37.2 |
| tls-h2-static-small | 64 484 | 61 232 | 60 388 | 41.3 | 46.9 |
| php | 31 311 | 24 003 | 22 656 | 83.5 | 103.4 |
The VM spread is larger than most of the gaps (the proxy runs were 78.4k, 78.7k and 92.6k); a bare-metal run with the load generator on separate cores is needed to settle the static, respond and proxy lanes.