Skip to content
github-actions[bot] edited this page Sep 9, 2026 · 210 revisions

Performance

Why this comparison is fair

These tables are a same-harness, same-origin, same-runner-class reverse-proxy comparison — not a blog-post bake-off of mismatched labs.

  • Same load generator (dotnet-httpclient), same origin process, same warmup/measure (2s / 8s), same concurrency ramp (8, 16, 32, 64), median of 3 GitHub Actions repeats.
  • Every reverse arm is three OS processes (load generator + origin child + proxy child). Origin-direct omits the proxy. Peers are never in-process with the client.
  • Same runner class per table: windows-latest / ubuntu-latest / macos-15-intel (4-core-class). Laptop High-perf and macos-latest are never mixed into these tables.
  • YARP is Yarp.ReverseProxy 2.3.0 with equivalent TLS/ALPN. nginx uses streaming knobs (keepalive 256, buffering off). Linux nginx (mainline + http_v3) is authoritative; Windows nginx has no QUIC — those cells are Not possible, not losses.
  • MITM is TWP-only (CONNECT + forged certs). nginx/YARP cannot MITM; Lite/Full tables are overhead vs TWP reverse, not vs peers. There are no MITM charts.
  • The product signal is TWP÷YARP (gated ≥ 0.85 reverse) and MITM÷Reverse (Lite ≥ 0.50, Full ≥ 0.50), not absolute RPS. Absolute RPS moves with runner heat; ratios are the claim.
  • Product fast paths (session-lite, skip-poll, compressed relay) are in-tree defaults for interception-off reverse — the same class of work YARP/nginx do. The harness does not disable TWP safety that peers also skip, and it does not retune to pass gates (PERF-GATES.md).
  • MITM leaf keys are a shared RSA/ECDSA pair per CertificateManager (one key for all forged leaves). That is an intentional RPS tradeoff: compromise of that material affects every host minted by that manager. Reverse/YARP/nginx paths do not mint leaves.

Numbers below are Release measurements with RpsLoadProbe. FiddlerCore is not compared (commercial debugger license; not a throughput peer).

For pooling knobs and certificate first-visit tuning, see Performance and pooling. For the local cool A/B lab, laptop tables, and profiling notes, see Performance Profiling.

Contents

Measurement environment

All three OS use the 4-core-class public-repo GitHub-hosted runners: Windows / Linux at 4 vCPU / 16 GiB / 14 GB SSD, macOS at macos-15-intel 4-core / 14 GB (not macos-latest). Same harness knobs (workflow_dispatch RPS saturation: warmup 2s / measure 8s; concurrency 8, 16, 32, 64; median of 3 repeats; --stop-on-slo-fail default on). Every --ramp arm is three OS processes (parent load generator + origin child + proxy child), except origin-direct arms (load gen + origin only). Prefer TWP÷YARP / TWP÷nginx ratios over absolute RPS.

Laptop High-perf / cool-paired Windows numbers live on Performance Local Lab. Do not mix those absolutes into the tables below.

Tiered cadence

Tier Mode When
Daily / per-PR compare-spot minutes
Milestone compare-terminate / compare-matrix ~1–2h
Editions compare-editions ~60 min (CLI / Plus / Intercept stress arms)
Cross-version (Gate 2) compare-cross-version ~1–2h vs committed 6.0 baselines
Release / wiki compare-product ~3–4h
Heavier tables compare-bodies / post / lossy / arch / bridges / tls-cost dispatch independently

See PERF-GATES.md.

Windows (GitHub-hosted windows-latest)

OS Windows Server (GitHub-hosted windows-latest)
CPU 4 logical processors
RAM 16 GiB
Runtime .NET 10.0.x
nginx nginx/Windows 1.31.3 (same-OS only; no QUIC)
YARP Yarp.ReverseProxy 2.3.0
Harness RpsLoadProbe Release; median of 3 repeats

Linux (GitHub-hosted ubuntu-latest)

OS Ubuntu 24.04.x LTS
CPU 4 logical processors (AMD EPYC; runners this pass were 7763 / 9V74)
RAM 16 GiB
Runtime .NET 10.0.11
nginx nginx/1.31.4 (nginx.org mainline, --with-http_v3_module)
YARP Yarp.ReverseProxy 2.3.0
Harness RpsLoadProbe Release; median of 3 repeats where noted

macOS (GitHub-hosted macos-15-intel)

OS macOS 15 (GitHub-hosted macos-15-intel, Intel x86_64)
CPU 4 logical processors
RAM 14 GB
Runtime .NET 10.0.x
nginx Homebrew nginx with --with-http_v3_module (workflow fails if missing)
MsQuic Homebrew libmsquic + openssl@3 on DYLD_LIBRARY_PATH / DYLD_FALLBACK_LIBRARY_PATH (QuicListener.IsSupported)
YARP Yarp.ReverseProxy 2.3.0
Harness RpsLoadProbe Release; median of 3 repeats where noted

Do not use macos-latest (Apple Silicon, 3-core / 7 GB) for publishable saturation numbers.

Saturation control

Calibration for the shared 4 vCPU loopback shape: how close client + origin are to saturated before ranking reverse peers. Tiny keep-alive GET. Median of 3 repeats @ 024bd68d — 34126812894. Warmup 2s / measure 8s; concurrency 8, 16, 32, 64. Block A % of origin-HttpClient uses median peak RPS. Blocks B/C use peer÷YARP / ÷nginx on median peak (not % of H1 origin). RPS cells embed median RSS / CPU for the proxy child plus its full descendant tree (serve-proxy → nginx master → workers); origin-direct samples the origin child. Product matrices below use matched dotnet-httpclient only (not bombardier).

pwsh tools/RpsLoadProbe/run-rps.ps1 -Mode compare-saturation

Block A — H1 plain

Arm Generator Notes
origin-direct dotnet-httpclient No proxy — origin ceiling under the same client used for publishable ratios
origin-direct-bombardier bombardier External client check (CI installs bombardier)
bare-reverse-http1 dotnet-httpclient Thin C# H1 reverse (BareHttp1ReverseProxy) — .NET runtime / loopback ceiling for a three-process reverse hop; not a product peer
nginx-reverse-http1 / yarp-reverse-http1 / twp-reverse-http1 dotnet-httpclient Product peers (medals among these three only)

Windows (windows-latest)

Arm Generator Sustain Peak % of origin-HttpClient
origin-direct dotnet-httpclient 50,248
(54 MiB / 40.0% CPU)
50,248
(54 MiB / 40.0% CPU)
100.0%
origin-direct-bombardier bombardier 39,040
(55 MiB / 24.0% CPU)
39,040
(55 MiB / 24.0% CPU)
77.7%
bare-reverse-http1 dotnet-httpclient 25,031
(58 MiB / 44.0% CPU)
25,031
(58 MiB / 44.0% CPU)
49.8%
nginx-reverse-http1 dotnet-httpclient 13,418
(123 MiB / 24.8% CPU)
13,418
(123 MiB / 24.8% CPU)
26.7%
yarp-reverse-http1 dotnet-httpclient 21,010
(89 MiB / 50.2% CPU)
21,010
(89 MiB / 50.2% CPU)
41.8%
twp-reverse-http1 dotnet-httpclient 🥇 24,500
(75 MiB / 47.9% CPU)
24,500
(75 MiB / 47.9% CPU)
48.8%

Linux (ubuntu-latest)

Arm Generator Sustain Peak % of origin-HttpClient
origin-direct dotnet-httpclient 71,493
(79 MiB / 42.6% CPU)
71,493
(79 MiB / 42.6% CPU)
100.0%
origin-direct-bombardier bombardier 42,014
(79 MiB / 34.9% CPU)
42,014
(79 MiB / 34.9% CPU)
58.8%
bare-reverse-http1 dotnet-httpclient 32,717
(66 MiB / 45.0% CPU)
32,717
(66 MiB / 45.0% CPU)
45.8%
nginx-reverse-http1 dotnet-httpclient 🥇 39,137
(73 MiB / 40.8% CPU)
39,137
(73 MiB / 40.8% CPU)
54.7%
yarp-reverse-http1 dotnet-httpclient 27,676
(114 MiB / 49.8% CPU)
27,676
(114 MiB / 49.8% CPU)
38.7%
twp-reverse-http1 dotnet-httpclient 32,666
(85 MiB / 50.5% CPU)
32,666
(85 MiB / 50.5% CPU)
45.7%

Reverse peers are about 50–46% of the origin-direct HttpClient peak on this runner class (Win TWP 50.0%, Lin TWP 45.6%). Prefer the % column over absolute RPS across runs. Bare and origin-direct are controls (not medal peers).

Block B — H2 TLS→H1

Peer ratios (÷YARP / ÷nginx) on median peak; RPS cells embed (MiB / CPU%).

Windows (windows-latest)

Arm Generator Sustain Peak ÷YARP ÷nginx
nginx-reverse-http2 dotnet-httpclient 7,862
(139 MiB / 24.7% CPU)
7,862
(139 MiB / 24.7% CPU)
0.27× 1.00×
yarp-reverse-http2 dotnet-httpclient 28,862
(94 MiB / 53.2% CPU)
28,862
(94 MiB / 53.2% CPU)
1.00× 3.67×
twp-reverse-http2-cleartext dotnet-httpclient 🥇 33,703
(103 MiB / 54.3% CPU)
33,703
(103 MiB / 54.3% CPU)
1.17× 4.29×

Linux (ubuntu-latest)

Arm Generator Sustain Peak ÷YARP ÷nginx
nginx-reverse-http2 dotnet-httpclient 14,871
(98 MiB / 19.3% CPU)
14,871
(98 MiB / 19.3% CPU)
0.51× 1.00×
yarp-reverse-http2 dotnet-httpclient 29,038
(122 MiB / 50.0% CPU)
29,038
(122 MiB / 50.0% CPU)
1.00× 1.95×
twp-reverse-http2-cleartext dotnet-httpclient 🥇 34,081
(123 MiB / 52.5% CPU)
34,081
(123 MiB / 52.5% CPU)
1.17× 2.29×

Block C — H3→H1

Same layout as Block B. Requires QuicListener; nginx only with http_v3_module (Windows nginx has no QUIC).

Windows (windows-latest)

Arm Generator Sustain Peak ÷YARP ÷nginx
nginx-reverse-http3-cleartext dotnet-httpclient Not possible (no QUIC) Not possible (no QUIC) — —
yarp-reverse-http3-cleartext dotnet-httpclient 🥇 14,836
(158 MiB / 52.2% CPU)
14,836
(158 MiB / 52.2% CPU)
1.00× —
twp-reverse-http3-cleartext dotnet-httpclient 14,310
(104 MiB / 44.9% CPU)
14,310
(104 MiB / 44.9% CPU)
0.96× —

Linux (ubuntu-latest)

Arm Generator Sustain Peak ÷YARP ÷nginx
nginx-reverse-http3-cleartext dotnet-httpclient 0
(104 MiB / 22.6% CPU)
15,166
(104 MiB / 22.6% CPU)
0.81× 1.00×
yarp-reverse-http3-cleartext dotnet-httpclient 18,684
(181 MiB / 50.3% CPU)
18,684
(181 MiB / 50.3% CPU)
1.00× 1.23×
twp-reverse-http3-cleartext dotnet-httpclient 🥇 20,638
(143 MiB / 50.7% CPU)
20,638
(143 MiB / 50.7% CPU)
1.10× 1.36×

How to read the tables

  • Reverse = bare transparent fixed-forward (no TWP plugins / interception). nginx knobs match TWP/YARP streaming (keepalive 256, proxy_buffering off). MITM = TWP-only table on the same Client×Origin wires: Lite = no-op handlers (unchanged-lite finish reuses reverse compressed relay); Full = mutating handlers that append up to four unique headers per direction (RPS harness adds one; product uses MitmCompressedRelayHelper — no probe name in library code). Remove/replace/non-unique header growth and body mutation still force full decode/re-encode. nginx/YARP cannot MITM. HTTP/3 has no cleartext client (QUIC always encrypted).
  • Sustainable = last concurrency that still met error/latency SLOs. Peak = highest RPS in that ramp.
  • 🥇 = best among TWP / nginx / YARP on Reverse rows (or saturation blocks): highest RPS; on an RPS tie, lower Memory (RSS) then lower CPU%. MITM is TWP-only. Lite÷Reverse / Full÷Reverse = TWP MITM lite or full sustain ÷ TWP Reverse sustain on the same Client×Origin from the same compare-product job.
  • Not possible = product cannot do that path. Not measured = path exists but no published number yet for that OS.
  • Product refresh: compare-product @ 024bd68d — 34126809918. Heavier/saturation/tls:
pwsh tools/RpsLoadProbe/run-rps.ps1 -Mode compare-product
pwsh tools/RpsLoadProbe/run-rps.ps1 -Mode compare-bodies
pwsh tools/RpsLoadProbe/run-rps.ps1 -Mode compare-post
pwsh tools/RpsLoadProbe/run-rps.ps1 -Mode compare-lossy
pwsh tools/RpsLoadProbe/run-rps.ps1 -Mode compare-tls-cost
pwsh tools/RpsLoadProbe/run-rps.ps1 -Mode compare-arch
pwsh tools/RpsLoadProbe/run-rps.ps1 -Mode compare-saturation

Windows — Titanium vs nginx vs YARP

Client / origin: HTTP version and whether TLS is used (plain = cleartext, TLS = encrypted, QUIC = HTTP/3).

Reverse

Median of 3 repeats on windows-latest (4 vCPU / 16 GiB). Bare reverse 5×5 @ 024bd68d — compare-product 34126809918. Warmup 2s / measure 8s; concurrency 8, 16, 32, 64. Prefer TWP÷peer ratios over absolute RPS. RPS cells include median RSS / CPU at the peak-RPS step as <br><sub>(MiB / CPU%)</sub>. nginx terminate peers use keepalive 256 + streaming buffers. Laptop High-perf / cool-paired numbers stay on the local lab.

Load generators: Reverse inbound H3 arms use dotnet-httpclient (http_version=3.0, RequestVersionExact). nginx/Windows is same-OS only (no QUIC).

Windows reverse

Client Origin TWP sustain TWP peak nginx sustain nginx peak YARP sustain YARP peak
HTTP/1 · plain HTTP/1 · plain 🥇 43148
(72 MiB / 48.4% CPU)
🥇 43148
(72 MiB / 48.4% CPU)
27494
(123 MiB / 24.8% CPU)
27494
(123 MiB / 24.8% CPU)
37946
(88 MiB / 50.5% CPU)
37946
(88 MiB / 50.5% CPU)
HTTP/1 · plain HTTP/1 · TLS 🥇 36643
(90 MiB / 50.4% CPU)
🥇 36643
(90 MiB / 50.4% CPU)
16661
(133 MiB / 24.7% CPU)
16661
(133 MiB / 24.7% CPU)
33622
(94 MiB / 48.9% CPU)
33622
(94 MiB / 48.9% CPU)
HTTP/1 · plain HTTP/2 · plain 🥇 62491
(116 MiB / 48.5% CPU)
🥇 62491
(116 MiB / 48.5% CPU)
Not possible Not possible 55620
(93 MiB / 52.7% CPU)
55620
(93 MiB / 52.7% CPU)
HTTP/1 · plain HTTP/2 · TLS 🥇 54245
(128 MiB / 44.3% CPU)
🥇 54245
(128 MiB / 44.3% CPU)
Not possible Not possible 49838
(96 MiB / 49.2% CPU)
49838
(96 MiB / 49.2% CPU)
HTTP/1 · plain HTTP/3 · QUIC 29032
(116 MiB / 48.6% CPU)
29032
(116 MiB / 48.6% CPU)
Not possible (no QUIC) Not possible (no QUIC) 🥇 29598
(124 MiB / 50.9% CPU)
🥇 29598
(124 MiB / 50.9% CPU)
HTTP/1 · TLS HTTP/1 · plain 🥇 36898
(88 MiB / 47% CPU)
🥇 36898
(88 MiB / 47% CPU)
17311
(140 MiB / 24.9% CPU)
17311
(140 MiB / 24.9% CPU)
32283
(102 MiB / 49.6% CPU)
32283
(102 MiB / 49.6% CPU)
HTTP/1 · TLS HTTP/1 · TLS 🥇 32801
(85 MiB / 48.7% CPU)
🥇 32801
(85 MiB / 48.7% CPU)
13080
(142 MiB / 24.9% CPU)
13080
(142 MiB / 24.9% CPU)
29170
(105 MiB / 50.9% CPU)
29170
(105 MiB / 50.9% CPU)
HTTP/1 · TLS HTTP/2 · plain 🥇 49296
(127 MiB / 45.9% CPU)
🥇 49296
(127 MiB / 45.9% CPU)
Not possible Not possible 45621
(107 MiB / 50.2% CPU)
45621
(107 MiB / 50.2% CPU)
HTTP/1 · TLS HTTP/2 · TLS 🥇 43714
(134 MiB / 45.7% CPU)
🥇 43714
(134 MiB / 45.7% CPU)
Not possible Not possible 42064
(113 MiB / 49.1% CPU)
42064
(113 MiB / 49.1% CPU)
HTTP/1 · TLS HTTP/3 · QUIC 25085
(120 MiB / 50.4% CPU)
25085
(120 MiB / 50.4% CPU)
Not possible (no QUIC) Not possible (no QUIC) 🥇 25200
(138 MiB / 52% CPU)
🥇 25200
(138 MiB / 52% CPU)
HTTP/2 · plain HTTP/1 · plain 🥇 55581
(97 MiB / 52.1% CPU)
🥇 55581
(97 MiB / 52.1% CPU)
Not possible Not possible 53465
(87 MiB / 50.3% CPU)
53465
(87 MiB / 50.3% CPU)
HTTP/2 · plain HTTP/1 · TLS 🥇 46471
(104 MiB / 51% CPU)
🥇 46471
(104 MiB / 51% CPU)
Not possible Not possible 44832
(97 MiB / 50.6% CPU)
44832
(97 MiB / 50.6% CPU)
HTTP/2 · plain HTTP/2 · plain 🥇 150625
(57 MiB / 29.1% CPU)
🥇 150625
(57 MiB / 29.1% CPU)
Not possible Not possible 88874
(93 MiB / 50.2% CPU)
88874
(93 MiB / 50.2% CPU)
HTTP/2 · plain HTTP/2 · TLS 🥇 122710
(62 MiB / 27.4% CPU)
🥇 122710
(62 MiB / 27.4% CPU)
Not possible Not possible 77808
(101 MiB / 48.3% CPU)
77808
(101 MiB / 48.3% CPU)
HTTP/2 · plain HTTP/3 · QUIC 🥇 42885
(141 MiB / 53.2% CPU)
🥇 42885
(141 MiB / 53.2% CPU)
Not possible (no QUIC) Not possible (no QUIC) 41031
(135 MiB / 51.7% CPU)
41031
(135 MiB / 51.7% CPU)
HTTP/2 · TLS HTTP/1 · plain 🥇 54084
(108 MiB / 51.7% CPU)
🥇 54084
(108 MiB / 51.7% CPU)
15726
(139 MiB / 24.3% CPU)
15726
(139 MiB / 24.3% CPU)
47838
(97 MiB / 51.2% CPU)
47838
(97 MiB / 51.2% CPU)
HTTP/2 · TLS HTTP/1 · TLS 🥇 47790
(103 MiB / 51.4% CPU)
🥇 47790
(103 MiB / 51.4% CPU)
11737
(143 MiB / 24.4% CPU)
11737
(143 MiB / 24.4% CPU)
41198
(95 MiB / 51.8% CPU)
41198
(95 MiB / 51.8% CPU)
HTTP/2 · TLS HTTP/2 · plain 🥇 140904
(74 MiB / 31.1% CPU)
🥇 140904
(74 MiB / 31.1% CPU)
Not possible Not possible 77150
(101 MiB / 53% CPU)
77150
(101 MiB / 53% CPU)
HTTP/2 · TLS HTTP/2 · TLS 🥇 116819
(75 MiB / 30.3% CPU)
🥇 116819
(75 MiB / 30.3% CPU)
Not possible Not possible 70121
(98 MiB / 48.3% CPU)
70121
(98 MiB / 48.3% CPU)
HTTP/2 · TLS HTTP/3 · QUIC 🥇 41432
(140 MiB / 50.9% CPU)
🥇 41432
(140 MiB / 50.9% CPU)
Not possible (no QUIC) Not possible (no QUIC) 37958
(134 MiB / 51.3% CPU)
37958
(134 MiB / 51.3% CPU)
HTTP/3 · QUIC HTTP/1 · plain 🥇 26401
(116 MiB / 44.6% CPU)
🥇 26401
(116 MiB / 44.6% CPU)
Not possible (no QUIC) Not possible (no QUIC) 25984
(149 MiB / 48.5% CPU)
25984
(149 MiB / 48.5% CPU)
HTTP/3 · QUIC HTTP/1 · TLS 🥇 23473
(126 MiB / 43.8% CPU)
🥇 23473
(126 MiB / 43.8% CPU)
Not possible (no QUIC) Not possible (no QUIC) 21408
(155 MiB / 49.8% CPU)
21408
(155 MiB / 49.8% CPU)
HTTP/3 · QUIC HTTP/2 · plain 🥇 46358
(147 MiB / 47.8% CPU)
🥇 46358
(147 MiB / 47.8% CPU)
Not possible (no H3 to H2) Not possible (no H3 to H2) 34679
(164 MiB / 49.7% CPU)
34679
(164 MiB / 49.7% CPU)
HTTP/3 · QUIC HTTP/2 · TLS 🥇 42019
(161 MiB / 42.9% CPU)
🥇 42019
(161 MiB / 42.9% CPU)
Not possible (no H3 to H2) Not possible (no H3 to H2) 35763
(163 MiB / 47.9% CPU)
35763
(163 MiB / 47.9% CPU)
HTTP/3 · QUIC HTTP/3 · QUIC 🥇 22676
(128 MiB / 45.5% CPU)
🥇 22676
(128 MiB / 45.5% CPU)
Not possible (no QUIC) Not possible (no QUIC) 18594
(181 MiB / 51.2% CPU)
18594
(181 MiB / 51.2% CPU)

MITM (TWP only)

Same Client×Origin wires with interception on (compare-product 34126809918). Lite = no-op handlers (unchanged-lite finish). Full = append-only header mutation (harness: one probe header each way; product: generic append-only relay via MitmCompressedRelayHelper). nginx/YARP cannot MITM. Lite÷Reverse / Full÷Reverse vs bare reverse (same job). Completion gate: Lite ≥ 0.50× and Full ≥ 0.50× reverse sustain @ c=64 (median of 3 GHA runs); reverse TWP÷YARP ≥ 0.85× (no nginx gate).

v1 append-only relay (2026-08-27): Pre-fix H2→H2 Full÷Reverse was 0.13–0.16× (32960766249). Post-fix @ df172718: H2 plain→H2 plain Full 0.77–0.79×, H3→H1 Full 0.91–0.93×. Current product floors are Lite ≥ 0.50× and Full ≥ 0.50× reverse (not a historical “all MITM ≥ 0.70×” claim).

v2 drop-only + non-unique append (2026-08-27): MitmStaticRebuildHelper rebuilds static HPACK/QPACK after 1–4 unique header drops; trailing non-unique appends stay on compressed relay. Same 0.50 / 0.50 floors apply to later GHA medians. H2 multiplexed Full is the tightest arm (often ~0.50–0.68×).

Client Origin Lite sustain Full sustain Lite÷Reverse Full÷Reverse
HTTP/1 · plain HTTP/1 · plain 42416
(78 MiB / 49.6% CPU)
41642
(82 MiB / 50.7% CPU)
0.98× 0.97×
HTTP/1 · plain HTTP/1 · TLS 35496
(93 MiB / 50.8% CPU)
35017
(94 MiB / 49.4% CPU)
0.97× 0.96×
HTTP/1 · plain HTTP/2 · plain 60830
(118 MiB / 50.1% CPU)
59596
(121 MiB / 49.7% CPU)
0.97× 0.95×
HTTP/1 · plain HTTP/2 · TLS 53754
(124 MiB / 47% CPU)
52767
(135 MiB / 48.7% CPU)
0.99× 0.97×
HTTP/1 · plain HTTP/3 · QUIC 28558
(112 MiB / 51.6% CPU)
28107
(114 MiB / 51% CPU)
0.98× 0.97×
HTTP/1 · TLS HTTP/1 · plain 35649
(93 MiB / 49.9% CPU)
35247
(94 MiB / 50% CPU)
0.97× 0.96×
HTTP/1 · TLS HTTP/1 · TLS 32815
(94 MiB / 48.2% CPU)
31979
(93 MiB / 49.9% CPU)
1× 0.97×
HTTP/1 · TLS HTTP/2 · plain 48342
(118 MiB / 47.9% CPU)
47289
(123 MiB / 50.1% CPU)
0.98× 0.96×
HTTP/1 · TLS HTTP/2 · TLS 43891
(134 MiB / 46.9% CPU)
43067
(125 MiB / 45.9% CPU)
1× 0.99×
HTTP/1 · TLS HTTP/3 · QUIC 25139
(116 MiB / 51.7% CPU)
24602
(116 MiB / 50% CPU)
1× 0.98×
HTTP/2 · plain HTTP/1 · plain 55126
(101 MiB / 51.5% CPU)
53456
(97 MiB / 53.2% CPU)
0.99× 0.96×
HTTP/2 · plain HTTP/1 · TLS 47504
(103 MiB / 54.9% CPU)
46365
(103 MiB / 53.5% CPU)
1.02× 1×
HTTP/2 · plain HTTP/2 · plain 99759
(70 MiB / 49.8% CPU)
93161
(74 MiB / 48.8% CPU)
0.66× 0.62×
HTTP/2 · plain HTTP/2 · TLS 87259
(79 MiB / 45.4% CPU)
82403
(77 MiB / 46.7% CPU)
0.71× 0.67×
HTTP/2 · plain HTTP/3 · QUIC 42663
(137 MiB / 52.5% CPU)
41785
(134 MiB / 54.6% CPU)
0.99× 0.97×
HTTP/2 · TLS HTTP/1 · plain 53275
(108 MiB / 53.8% CPU)
51033
(112 MiB / 53% CPU)
0.99× 0.94×
HTTP/2 · TLS HTTP/1 · TLS 46834
(106 MiB / 54% CPU)
44869
(111 MiB / 51.2% CPU)
0.98× 0.94×
HTTP/2 · TLS HTTP/2 · plain 95259
(97 MiB / 49.4% CPU)
89191
(89 MiB / 46.6% CPU)
0.68× 0.63×
HTTP/2 · TLS HTTP/2 · TLS 84908
(85 MiB / 44.9% CPU)
79447
(95 MiB / 45.9% CPU)
0.73× 0.68×
HTTP/2 · TLS HTTP/3 · QUIC 41573
(144 MiB / 52.7% CPU)
40421
(146 MiB / 52.7% CPU)
1× 0.98×
HTTP/3 · QUIC HTTP/1 · plain 24842
(114 MiB / 45.9% CPU)
23923
(124 MiB / 46.3% CPU)
0.94× 0.91×
HTTP/3 · QUIC HTTP/1 · TLS 22186
(131 MiB / 45.5% CPU)
21112
(117 MiB / 45.2% CPU)
0.95× 0.9×
HTTP/3 · QUIC HTTP/2 · plain 42900
(157 MiB / 51.1% CPU)
41039
(146 MiB / 50.2% CPU)
0.93× 0.89×
HTTP/3 · QUIC HTTP/2 · TLS 40022
(158 MiB / 47.9% CPU)
39065
(157 MiB / 48.6% CPU)
0.95× 0.93×
HTTP/3 · QUIC HTTP/3 · QUIC 20576
(129 MiB / 48.4% CPU)
20055
(129 MiB / 50.6% CPU)
0.91× 0.88×

Linux — Titanium vs nginx vs YARP

Reverse

Median of 3 repeats on ubuntu-latest (4 vCPU / 16 GiB). Bare reverse 5×5 @ 024bd68d — compare-product 34126809918. Warmup 2s / measure 8s; concurrency 8, 16, 32, 64. Linux nginx is the authoritative nginx baseline. nginx terminate peers use keepalive 256 + streaming buffers. The RPS workflow installs nginx.org mainline (http_v3_module) and libmsquic. Prefer ratios over absolute RPS.

Linux reverse

Client Origin TWP sustain TWP peak nginx sustain nginx peak YARP sustain YARP peak
HTTP/1 · plain HTTP/1 · plain 45442
(82 MiB / 50% CPU)
45442
(82 MiB / 50% CPU)
🥇 54602
(73 MiB / 39.5% CPU)
🥇 54602
(73 MiB / 39.5% CPU)
40514
(117 MiB / 49.4% CPU)
40514
(117 MiB / 49.4% CPU)
HTTP/1 · plain HTTP/1 · TLS 36887
(106 MiB / 49.2% CPU)
36887
(106 MiB / 49.2% CPU)
🥇 43002
(90 MiB / 41.1% CPU)
🥇 43002
(90 MiB / 41.1% CPU)
32675
(137 MiB / 49% CPU)
32675
(137 MiB / 49% CPU)
HTTP/1 · plain HTTP/2 · plain 🥇 58774
(138 MiB / 50.8% CPU)
🥇 58774
(138 MiB / 50.8% CPU)
Not possible Not possible 52131
(123 MiB / 48.3% CPU)
52131
(123 MiB / 48.3% CPU)
HTTP/1 · plain HTTP/2 · TLS 🥇 45682
(155 MiB / 49.4% CPU)
🥇 45682
(155 MiB / 49.4% CPU)
Not possible Not possible 43753
(127 MiB / 47.3% CPU)
43753
(127 MiB / 47.3% CPU)
HTTP/1 · plain HTTP/3 · QUIC 🥇 31312
(144 MiB / 54.6% CPU)
🥇 31312
(144 MiB / 54.6% CPU)
Not possible (no QUIC) Not possible (no QUIC) 29231
(156 MiB / 48.4% CPU)
29231
(156 MiB / 48.4% CPU)
HTTP/1 · TLS HTTP/1 · plain 34763
(109 MiB / 49.9% CPU)
34763
(109 MiB / 49.9% CPU)
🥇 42628
(100 MiB / 41.1% CPU)
🥇 42628
(100 MiB / 41.1% CPU)
31712
(132 MiB / 49.9% CPU)
31712
(132 MiB / 49.9% CPU)
HTTP/1 · TLS HTTP/1 · TLS 30596
(107 MiB / 47.7% CPU)
30596
(107 MiB / 47.7% CPU)
🥇 35876
(104 MiB / 40.9% CPU)
🥇 35876
(104 MiB / 40.9% CPU)
27094
(137 MiB / 49.5% CPU)
27094
(137 MiB / 49.5% CPU)
HTTP/1 · TLS HTTP/2 · plain 🥇 43784
(145 MiB / 50.1% CPU)
🥇 43784
(145 MiB / 50.1% CPU)
Not possible Not possible 39595
(140 MiB / 49.2% CPU)
39595
(140 MiB / 49.2% CPU)
HTTP/1 · TLS HTTP/2 · TLS 🥇 38412
(155 MiB / 48.6% CPU)
🥇 38412
(155 MiB / 48.6% CPU)
Not possible Not possible 35014
(142 MiB / 48.2% CPU)
35014
(142 MiB / 48.2% CPU)
HTTP/1 · TLS HTTP/3 · QUIC 🥇 27078
(154 MiB / 53.3% CPU)
🥇 27078
(154 MiB / 53.3% CPU)
Not possible (no QUIC) Not possible (no QUIC) 24650
(167 MiB / 49.1% CPU)
24650
(167 MiB / 49.1% CPU)
HTTP/2 · plain HTTP/1 · plain 🥇 50993
(117 MiB / 51.6% CPU)
🥇 50993
(117 MiB / 51.6% CPU)
Not possible Not possible 49389
(114 MiB / 47.9% CPU)
49389
(114 MiB / 47.9% CPU)
HTTP/2 · plain HTTP/1 · TLS 🥇 40178
(129 MiB / 50.5% CPU)
🥇 40178
(129 MiB / 50.5% CPU)
Not possible Not possible 38512
(124 MiB / 48.8% CPU)
38512
(124 MiB / 48.8% CPU)
HTTP/2 · plain HTTP/2 · plain 🥇 112467
(91 MiB / 38.3% CPU)
🥇 112467
(91 MiB / 38.3% CPU)
Not possible Not possible 68404
(124 MiB / 46.3% CPU)
68404
(124 MiB / 46.3% CPU)
HTTP/2 · plain HTTP/2 · TLS 🥇 86598
(91 MiB / 37% CPU)
🥇 86598
(91 MiB / 37% CPU)
Not possible Not possible 55654
(126 MiB / 44.9% CPU)
55654
(126 MiB / 44.9% CPU)
HTTP/2 · plain HTTP/3 · QUIC 34438
(156 MiB / 50.5% CPU)
34438
(156 MiB / 50.5% CPU)
Not possible (no QUIC) Not possible (no QUIC) 🥇 34482
(157 MiB / 45.7% CPU)
🥇 34482
(157 MiB / 45.7% CPU)
HTTP/2 · TLS HTTP/1 · plain 🥇 47100
(121 MiB / 51.4% CPU)
🥇 47100
(121 MiB / 51.4% CPU)
21439
(99 MiB / 18.8% CPU)
21439
(99 MiB / 18.8% CPU)
40159
(122 MiB / 48.2% CPU)
40159
(122 MiB / 48.2% CPU)
HTTP/2 · TLS HTTP/1 · TLS 🥇 38515
(120 MiB / 49.1% CPU)
🥇 38515
(120 MiB / 49.1% CPU)
18594
(109 MiB / 19.3% CPU)
18594
(109 MiB / 19.3% CPU)
34597
(129 MiB / 48.5% CPU)
34597
(129 MiB / 48.5% CPU)
HTTP/2 · TLS HTTP/2 · plain 🥇 103058
(99 MiB / 38.9% CPU)
🥇 103058
(99 MiB / 38.9% CPU)
Not possible Not possible 54026
(123 MiB / 46.3% CPU)
54026
(123 MiB / 46.3% CPU)
HTTP/2 · TLS HTTP/2 · TLS 🥇 77878
(99 MiB / 35.6% CPU)
🥇 77878
(99 MiB / 35.6% CPU)
Not possible Not possible 48036
(131 MiB / 45% CPU)
48036
(131 MiB / 45% CPU)
HTTP/2 · TLS HTTP/3 · QUIC 🥇 32383
(166 MiB / 49.6% CPU)
🥇 32383
(166 MiB / 49.6% CPU)
Not possible (no QUIC) Not possible (no QUIC) 30252
(163 MiB / 45.2% CPU)
30252
(163 MiB / 45.2% CPU)
HTTP/3 · QUIC HTTP/1 · plain 🥇 27304
(150 MiB / 51.3% CPU)
🥇 27304
(150 MiB / 51.3% CPU)
0
(108 MiB / 21.3% CPU)
22735
(108 MiB / 21.3% CPU)
25272
(191 MiB / 49.3% CPU)
25272
(191 MiB / 49.3% CPU)
HTTP/3 · QUIC HTTP/1 · TLS 🥇 21505
(165 MiB / 48.8% CPU)
🥇 21505
(165 MiB / 48.8% CPU)
0
(119 MiB / 22.4% CPU)
17246
(119 MiB / 22.4% CPU)
21129
(197 MiB / 49.1% CPU)
21129
(197 MiB / 49.1% CPU)
HTTP/3 · QUIC HTTP/2 · plain 🥇 36800
(166 MiB / 54.4% CPU)
🥇 36800
(166 MiB / 54.4% CPU)
Not possible (no H3 to H2) Not possible (no H3 to H2) 32383
(198 MiB / 47.8% CPU)
32383
(198 MiB / 47.8% CPU)
HTTP/3 · QUIC HTTP/2 · TLS 🥇 33722
(166 MiB / 51.6% CPU)
🥇 33722
(166 MiB / 51.6% CPU)
Not possible (no H3 to H2) Not possible (no H3 to H2) 30263
(203 MiB / 46.8% CPU)
30263
(203 MiB / 46.8% CPU)
HTTP/3 · QUIC HTTP/3 · QUIC 🥇 25361
(165 MiB / 44.6% CPU)
🥇 25361
(165 MiB / 44.6% CPU)
Not possible (no QUIC) Not possible (no QUIC) 21111
(209 MiB / 45.8% CPU)
21111
(209 MiB / 45.8% CPU)

Tiny JSON reverse is nginx’s best case on Linux

The tiny keep-alive GET (small JSON, no request body) is where a native C reverse proxy spends almost no time in user code. On Linux, nginx often leads HTTP/1 → HTTP/1 (plain and TLS) because that path is a handful of epoll + writev loops with keepalive 256 and buffering off — the same shape the harness uses for TWP and YARP.

That is not a TWP regression and not the product gate. The gated signal is TWP÷YARP ≥ 0.85 on every reverse wire. nginx Not possible cells (H2/H3 upstream, Windows QUIC) are omitted from charts rather than plotted as zero. Multiplexed H2→H2 is TWP’s best reverse case (session-lite + compressed relay); nginx cannot peer that wire.

Why isn’t HTTP/3 > HTTP/2 > HTTP/1 in raw RPS?

Raw RPS on this probe is not “newer protocol = faster.” HTTP/3 (QUIC/UDP, TLS 1.3, QPACK) does more CPU per tiny GET than HTTP/2-over-TCP or HTTP/1 keep-alive. H3 shines under loss, HOL blocking, and connection migration — see the lossy / high-RTT tables — not on a lossless 4-core loopback microbenchmark.

HTTP/2 multiplexed same-protocol reverse can beat HTTP/1 here because one TCP connection carries many streams and TWP’s compressed-relay path avoids per-stream session objects. HTTP/1 still wins some terminate-lite H1→H1 wires (especially vs nginx on Linux). Compare within a protocol pair, using TWP÷YARP, not a stacked H3 > H2 > H1 ranking.

MITM (TWP only)

Same Client×Origin wires with interception on (compare-product 34126809918). Lite = no-op handlers (unchanged-lite finish). Full = append-only header mutation (harness: one probe header each way; product: generic append-only relay via MitmCompressedRelayHelper). nginx/YARP cannot MITM. Lite÷Reverse / Full÷Reverse vs bare reverse (same job). Completion gate: Lite ≥ 0.50× and Full ≥ 0.50× reverse sustain @ c=64 (median of 3 GHA runs); reverse TWP÷YARP ≥ 0.85× (no nginx gate).

v1 append-only relay (2026-08-27): Pre-fix H2→H2 Full÷Reverse was 0.13–0.16× (32960766249). Post-fix @ df172718: H2 plain→H2 plain Full 0.77–0.79×, H3→H1 Full 0.91–0.93×. Current product floors are Lite ≥ 0.50× and Full ≥ 0.50× reverse (not a historical “all MITM ≥ 0.70×” claim).

v2 drop-only + non-unique append (2026-08-27): MitmStaticRebuildHelper rebuilds static HPACK/QPACK after 1–4 unique header drops; trailing non-unique appends stay on compressed relay. Same 0.50 / 0.50 floors apply to later GHA medians. H2 multiplexed Full is the tightest arm (often ~0.50–0.68×).

Client Origin Lite sustain Full sustain Lite÷Reverse Full÷Reverse
HTTP/1 · plain HTTP/1 · plain 44176
(94 MiB / 50.8% CPU)
39967
(95 MiB / 50% CPU)
0.97× 0.88×
HTTP/1 · plain HTTP/1 · TLS 33446
(109 MiB / 50.5% CPU)
33296
(111 MiB / 49.5% CPU)
0.91× 0.9×
HTTP/1 · plain HTTP/2 · plain 51914
(136 MiB / 53.8% CPU)
50316
(126 MiB / 53.7% CPU)
0.88× 0.86×
HTTP/1 · plain HTTP/2 · TLS 45010
(147 MiB / 50.3% CPU)
41890
(143 MiB / 50.5% CPU)
0.99× 0.92×
HTTP/1 · plain HTTP/3 · QUIC 29698
(144 MiB / 55.2% CPU)
29205
(154 MiB / 53.7% CPU)
0.95× 0.93×
HTTP/1 · TLS HTTP/1 · plain 32973
(115 MiB / 50.4% CPU)
30304
(115 MiB / 50.2% CPU)
0.95× 0.87×
HTTP/1 · TLS HTTP/1 · TLS 28337
(113 MiB / 49% CPU)
28230
(112 MiB / 48.3% CPU)
0.93× 0.92×
HTTP/1 · TLS HTTP/2 · plain 40580
(144 MiB / 51.3% CPU)
38701
(142 MiB / 51.1% CPU)
0.93× 0.88×
HTTP/1 · TLS HTTP/2 · TLS 35623
(165 MiB / 50% CPU)
34976
(157 MiB / 49.8% CPU)
0.93× 0.91×
HTTP/1 · TLS HTTP/3 · QUIC 24740
(158 MiB / 53.1% CPU)
25654
(162 MiB / 53.3% CPU)
0.91× 0.95×
HTTP/2 · plain HTTP/1 · plain 44972
(117 MiB / 53.5% CPU)
45432
(124 MiB / 53.5% CPU)
0.88× 0.89×
HTTP/2 · plain HTTP/1 · TLS 38000
(130 MiB / 51.6% CPU)
34677
(126 MiB / 51.2% CPU)
0.95× 0.86×
HTTP/2 · plain HTTP/2 · plain 70311
(114 MiB / 52.2% CPU)
62514
(110 MiB / 50.1% CPU)
0.63× 0.56×
HTTP/2 · plain HTTP/2 · TLS 56973
(106 MiB / 47.7% CPU)
53478
(106 MiB / 47.1% CPU)
0.66× 0.62×
HTTP/2 · plain HTTP/3 · QUIC 32982
(160 MiB / 50.7% CPU)
31395
(150 MiB / 50.6% CPU)
0.96× 0.91×
HTTP/2 · TLS HTTP/1 · plain 42980
(120 MiB / 52.6% CPU)
40591
(123 MiB / 52.8% CPU)
0.91× 0.86×
HTTP/2 · TLS HTTP/1 · TLS 37190
(131 MiB / 51.1% CPU)
33561
(133 MiB / 50.4% CPU)
0.97× 0.87×
HTTP/2 · TLS HTTP/2 · plain 65213
(122 MiB / 49.8% CPU)
59028
(122 MiB / 48.6% CPU)
0.63× 0.57×
HTTP/2 · TLS HTTP/2 · TLS 57395
(114 MiB / 45.9% CPU)
52587
(114 MiB / 45.6% CPU)
0.74× 0.68×
HTTP/2 · TLS HTTP/3 · QUIC 32026
(166 MiB / 50.4% CPU)
30999
(167 MiB / 51% CPU)
0.99× 0.96×
HTTP/3 · QUIC HTTP/1 · plain 27096
(153 MiB / 52.9% CPU)
25506
(160 MiB / 52.9% CPU)
0.99× 0.93×
HTTP/3 · QUIC HTTP/1 · TLS 20462
(165 MiB / 49.3% CPU)
22732
(161 MiB / 49.6% CPU)
0.95× 1.06×
HTTP/3 · QUIC HTTP/2 · plain 31264
(168 MiB / 55.8% CPU)
34476
(173 MiB / 55.9% CPU)
0.85× 0.94×
HTTP/3 · QUIC HTTP/2 · TLS 27463
(166 MiB / 52.5% CPU)
30491
(168 MiB / 52.8% CPU)
0.81× 0.9×
HTTP/3 · QUIC HTTP/3 · QUIC 20207
(161 MiB / 50.1% CPU)
23463
(166 MiB / 50.6% CPU)
0.8× 0.93×

macOS — Titanium vs nginx vs YARP

Numbers are filled by tools/RpsLoadProbe/apply-wiki-paste.ps1 after compare-product on macos-15-intel (placeholder headers match the Linux table shape).

Reverse

Median of 3 repeats on macos-15-intel (4-core / 14 GB). Bare reverse 5×5 @ 024bd68d — compare-product 34126809918. Warmup 2s / measure 8s; concurrency 8, 16, 32, 64. Prefer TWP÷peer ratios over absolute RPS. RPS cells include median RSS / CPU at the peak-RPS step as <br><sub>(MiB / CPU%)</sub>. The RPS workflow installs Homebrew nginx (http_v3_module), Homebrew libmsquic (+ DYLD_*), and YARP. Do not publish from macos-latest (3-core / 7 GB).

macOS reverse

Client Origin TWP sustain TWP peak nginx sustain nginx peak YARP sustain YARP peak
HTTP/1 · plain HTTP/1 · plain 🥇 15024
(84 MiB / 34.6% CPU)
🥇 15024
(84 MiB / 34.6% CPU)
7464
(51 MiB / 13.8% CPU)
7464
(51 MiB / 13.8% CPU)
11453
(105 MiB / 35.2% CPU)
11453
(105 MiB / 35.2% CPU)
HTTP/1 · plain HTTP/1 · TLS 🥇 11517
(94 MiB / 34.2% CPU)
🥇 11517
(94 MiB / 34.2% CPU)
6366
(73 MiB / 20.3% CPU)
6366
(73 MiB / 20.3% CPU)
11162
(122 MiB / 38.4% CPU)
11162
(122 MiB / 38.4% CPU)
HTTP/1 · plain HTTP/2 · plain 17744
(89 MiB / 36.4% CPU)
17744
(89 MiB / 36.4% CPU)
Not possible Not possible 🥇 22656
(111 MiB / 33.3% CPU)
🥇 22656
(111 MiB / 33.3% CPU)
HTTP/1 · plain HTTP/2 · TLS 14182
(105 MiB / 36.8% CPU)
14182
(105 MiB / 36.8% CPU)
Not possible Not possible 🥇 18762
(112 MiB / 34.9% CPU)
🥇 18762
(112 MiB / 34.9% CPU)
HTTP/1 · plain HTTP/3 · QUIC 5226
(91 MiB / 42.5% CPU)
5226
(91 MiB / 42.5% CPU)
Not possible (no QUIC) Not possible (no QUIC) 🥇 5914
(114 MiB / 35.4% CPU)
🥇 5914
(114 MiB / 35.4% CPU)
HTTP/1 · TLS HTTP/1 · plain 🥇 10608
(95 MiB / 33.2% CPU)
🥇 10608
(95 MiB / 33.2% CPU)
6819
(73 MiB / 13.4% CPU)
6819
(73 MiB / 13.4% CPU)
9148
(118 MiB / 34.2% CPU)
9148
(118 MiB / 34.2% CPU)
HTTP/1 · TLS HTTP/1 · TLS 🥇 8936
(98 MiB / 34.5% CPU)
🥇 8936
(98 MiB / 34.5% CPU)
6814
(81 MiB / 19.5% CPU)
6814
(81 MiB / 19.5% CPU)
8755
(130 MiB / 35% CPU)
8755
(130 MiB / 35% CPU)
HTTP/1 · TLS HTTP/2 · plain 14872
(97 MiB / 35.8% CPU)
14872
(97 MiB / 35.8% CPU)
Not possible Not possible 🥇 16912
(116 MiB / 32.6% CPU)
🥇 16912
(116 MiB / 32.6% CPU)
HTTP/1 · TLS HTTP/2 · TLS 13282
(151 MiB / 33.6% CPU)
13282
(151 MiB / 33.6% CPU)
Not possible Not possible 🥇 13433
(119 MiB / 31.7% CPU)
🥇 13433
(119 MiB / 31.7% CPU)
HTTP/1 · TLS HTTP/3 · QUIC 3511
(98 MiB / 47.2% CPU)
3511
(98 MiB / 47.2% CPU)
Not possible (no QUIC) Not possible (no QUIC) 🥇 5856
(124 MiB / 34% CPU)
🥇 5856
(124 MiB / 34% CPU)
HTTP/2 · plain HTTP/1 · plain 🥇 20764
(89 MiB / 39.2% CPU)
🥇 20764
(89 MiB / 39.2% CPU)
Not possible Not possible 17011
(105 MiB / 38.1% CPU)
17011
(105 MiB / 38.1% CPU)
HTTP/2 · plain HTTP/1 · TLS 13358
(98 MiB / 37.9% CPU)
13358
(98 MiB / 37.9% CPU)
Not possible Not possible 🥇 14990
(117 MiB / 41.9% CPU)
🥇 14990
(117 MiB / 41.9% CPU)
HTTP/2 · plain HTTP/2 · plain 🥇 44499
(74 MiB / 28.2% CPU)
🥇 44499
(74 MiB / 28.2% CPU)
Not possible Not possible 24595
(106 MiB / 36.3% CPU)
24595
(106 MiB / 36.3% CPU)
HTTP/2 · plain HTTP/2 · TLS 🥇 27865
(78 MiB / 28.9% CPU)
🥇 27865
(78 MiB / 28.9% CPU)
Not possible Not possible 23088
(113 MiB / 36.2% CPU)
23088
(113 MiB / 36.2% CPU)
HTTP/2 · plain HTTP/3 · QUIC 6007
(96 MiB / 44.8% CPU)
6007
(96 MiB / 44.8% CPU)
Not possible (no QUIC) Not possible (no QUIC) 🥇 7284
(116 MiB / 33.9% CPU)
🥇 7284
(116 MiB / 33.9% CPU)
HTTP/2 · TLS HTTP/1 · plain 🥇 19696
(93 MiB / 40.1% CPU)
🥇 19696
(93 MiB / 40.1% CPU)
11601
(72 MiB / 14.7% CPU)
11601
(72 MiB / 14.7% CPU)
14579
(112 MiB / 39.1% CPU)
14579
(112 MiB / 39.1% CPU)
HTTP/2 · TLS HTTP/1 · TLS 🥇 14310
(98 MiB / 40.2% CPU)
🥇 14310
(98 MiB / 40.2% CPU)
9839
(91 MiB / 14.9% CPU)
9839
(91 MiB / 14.9% CPU)
11766
(122 MiB / 41.5% CPU)
11766
(122 MiB / 41.5% CPU)
HTTP/2 · TLS HTTP/2 · plain 🥇 43713
(82 MiB / 29.7% CPU)
🥇 43713
(82 MiB / 29.7% CPU)
Not possible Not possible 25843
(113 MiB / 37.6% CPU)
25843
(113 MiB / 37.6% CPU)
HTTP/2 · TLS HTTP/2 · TLS 🥇 31290
(85 MiB / 28% CPU)
🥇 31290
(85 MiB / 28% CPU)
Not possible Not possible 28790
(117 MiB / 37.3% CPU)
28790
(117 MiB / 37.3% CPU)
HTTP/2 · TLS HTTP/3 · QUIC 🥇 7116
(109 MiB / 43.4% CPU)
🥇 7116
(109 MiB / 43.4% CPU)
Not possible (no QUIC) Not possible (no QUIC) 6614
(123 MiB / 36.6% CPU)
6614
(123 MiB / 36.6% CPU)
HTTP/3 · QUIC HTTP/1 · plain 5422
(94 MiB / 39.6% CPU)
5422
(94 MiB / 39.6% CPU)
0
(63 MiB / 11.4% CPU)
9594
(63 MiB / 11.4% CPU)
🥇 7112
(166 MiB / 35.9% CPU)
🥇 7112
(166 MiB / 35.9% CPU)
HTTP/3 · QUIC HTTP/1 · TLS 5401
(117 MiB / 40.8% CPU)
5401
(117 MiB / 40.8% CPU)
0
(66 MiB / 11.9% CPU)
6561
(66 MiB / 11.9% CPU)
🥇 5996
(175 MiB / 36.7% CPU)
🥇 5996
(175 MiB / 36.7% CPU)
HTTP/3 · QUIC HTTP/2 · plain 7072
(98 MiB / 40.3% CPU)
7072
(98 MiB / 40.3% CPU)
Not possible (no H3 to H2) Not possible (no H3 to H2) 🥇 8446
(172 MiB / 34% CPU)
🥇 8446
(172 MiB / 34% CPU)
HTTP/3 · QUIC HTTP/2 · TLS 6213
(106 MiB / 39% CPU)
6213
(106 MiB / 39% CPU)
Not possible (no H3 to H2) Not possible (no H3 to H2) 🥇 7921
(167 MiB / 34.9% CPU)
🥇 7921
(167 MiB / 34.9% CPU)
HTTP/3 · QUIC HTTP/3 · QUIC 4167
(97 MiB / 38.6% CPU)
4167
(97 MiB / 38.6% CPU)
Not possible (no QUIC) Not possible (no QUIC) 🥇 4703
(188 MiB / 30.7% CPU)
🥇 4703
(188 MiB / 30.7% CPU)

MITM (TWP only)

Same Client×Origin wires with interception on (compare-product 34126809918). Lite = no-op handlers (unchanged-lite finish). Full = append-only header mutation (harness: one probe header each way; product: generic append-only relay via MitmCompressedRelayHelper). nginx/YARP cannot MITM. Lite÷Reverse / Full÷Reverse vs bare reverse (same job). Completion gate: Lite ≥ 0.50× and Full ≥ 0.50× reverse sustain @ c=64 (median of 3 GHA runs); reverse TWP÷YARP ≥ 0.85× (no nginx gate).

v1 append-only relay (2026-08-27): Pre-fix H2→H2 Full÷Reverse was 0.13–0.16× (32960766249). Post-fix @ df172718: H2 plain→H2 plain Full 0.77–0.79×, H3→H1 Full 0.91–0.93×. Current product floors are Lite ≥ 0.50× and Full ≥ 0.50× reverse (not a historical “all MITM ≥ 0.70×” claim).

v2 drop-only + non-unique append (2026-08-27): MitmStaticRebuildHelper rebuilds static HPACK/QPACK after 1–4 unique header drops; trailing non-unique appends stay on compressed relay. Same 0.50 / 0.50 floors apply to later GHA medians. H2 multiplexed Full is the tightest arm (often ~0.50–0.68×).

Client Origin Lite sustain Full sustain Lite÷Reverse Full÷Reverse
HTTP/1 · plain HTTP/1 · plain 19900
(85 MiB / 35% CPU)
16913
(86 MiB / 35.3% CPU)
1.32× 1.13×
HTTP/1 · plain HTTP/1 · TLS 16485
(95 MiB / 39.2% CPU)
15930
(95 MiB / 38.2% CPU)
1.43× 1.38×
HTTP/1 · plain HTTP/2 · plain 21395
(94 MiB / 39.2% CPU)
22098
(94 MiB / 38.6% CPU)
1.21× 1.25×
HTTP/1 · plain HTTP/2 · TLS 18305
(112 MiB / 38.2% CPU)
17064
(108 MiB / 38.8% CPU)
1.29× 1.2×
HTTP/1 · plain HTTP/3 · QUIC 5206
(92 MiB / 45.1% CPU)
4197
(92 MiB / 44.5% CPU)
1× 0.8×
HTTP/1 · TLS HTTP/1 · plain 13388
(97 MiB / 34.1% CPU)
9514
(97 MiB / 33.5% CPU)
1.26× 0.9×
HTTP/1 · TLS HTTP/1 · TLS 11780
(99 MiB / 36.4% CPU)
8823
(100 MiB / 35.4% CPU)
1.32× 0.99×
HTTP/1 · TLS HTTP/2 · plain 10530
(99 MiB / 36.1% CPU)
10040
(103 MiB / 36% CPU)
0.71× 0.68×
HTTP/1 · TLS HTTP/2 · TLS 8486
(155 MiB / 32.1% CPU)
7775
(169 MiB / 33.9% CPU)
0.64× 0.59×
HTTP/1 · TLS HTTP/3 · QUIC 3027
(105 MiB / 48.8% CPU)
2898
(105 MiB / 48.4% CPU)
0.86× 0.83×
HTTP/2 · plain HTTP/1 · plain 16212
(91 MiB / 39.3% CPU)
14686
(91 MiB / 39.6% CPU)
0.78× 0.71×
HTTP/2 · plain HTTP/1 · TLS 13272
(98 MiB / 41.8% CPU)
11864
(98 MiB / 40.3% CPU)
0.99× 0.89×
HTTP/2 · plain HTTP/2 · plain 28429
(80 MiB / 35.7% CPU)
23104
(78 MiB / 34.1% CPU)
0.64× 0.52×
HTTP/2 · plain HTTP/2 · TLS 23386
(83 MiB / 35.7% CPU)
20906
(84 MiB / 35.3% CPU)
0.84× 0.75×
HTTP/2 · plain HTTP/3 · QUIC 4491
(96 MiB / 45.1% CPU)
4985
(97 MiB / 45.2% CPU)
0.75× 0.83×
HTTP/2 · TLS HTTP/1 · plain 15975
(94 MiB / 40.1% CPU)
15511
(92 MiB / 40.5% CPU)
0.81× 0.79×
HTTP/2 · TLS HTTP/1 · TLS 16070
(97 MiB / 41.1% CPU)
12680
(98 MiB / 43.1% CPU)
1.12× 0.89×
HTTP/2 · TLS HTTP/2 · plain 25016
(90 MiB / 33.1% CPU)
26369
(87 MiB / 35.7% CPU)
0.57× 0.6×
HTTP/2 · TLS HTTP/2 · TLS 19676
(90 MiB / 31.6% CPU)
24635
(88 MiB / 34.8% CPU)
0.63× 0.79×
HTTP/2 · TLS HTTP/3 · QUIC 5379
(106 MiB / 43.2% CPU)
6026
(106 MiB / 40.5% CPU)
0.76× 0.85×
HTTP/3 · QUIC HTTP/1 · plain 4430
(103 MiB / 41.2% CPU)
4710
(103 MiB / 38.8% CPU)
0.82× 0.87×
HTTP/3 · QUIC HTTP/1 · TLS 4132
(116 MiB / 41.7% CPU)
3975
(115 MiB / 41.4% CPU)
0.77× 0.74×
HTTP/3 · QUIC HTTP/2 · plain 5296
(100 MiB / 38.6% CPU)
5368
(100 MiB / 40.1% CPU)
0.75× 0.76×
HTTP/3 · QUIC HTTP/2 · TLS 5082
(108 MiB / 37.2% CPU)
5304
(109 MiB / 39.7% CPU)
0.82× 0.85×
HTTP/3 · QUIC HTTP/3 · QUIC 3482
(97 MiB / 36.4% CPU)
3026
(96 MiB / 37.8% CPU)
0.84× 0.73×

Editions (CLI / Plus / Intercept)

Note: twp-reverse-http1 and other library rows use Core with probe-tuned settings (no logging, no Via header, probe-warmed certs). Edition rows use titanium run -c twp.yaml product defaults — prefer the ÷baseline ratio column over absolute RPS. Inspector GUI is not spawnable in the harness; session-path overhead is twp-cli-intercept-http1 (route RequestHeaderSet transform).

Gates prioritize SLO survival under load and reasonable overhead vs baseline. Pre-origin Plus middleware (CIDR/WAF/JWT/rate-limit) runs on H1 terminate-lite without SessionEventArgs; JWT caches successful bearer validations. Thresholds — see PERF-GATES.md.

Median of 3 repeats @ 6d2a7c9d. Source: Actions 33259699099 (compare-editions). Warmup 2s / measure 8s; concurrency 8–64; sustain = median peak RPS among SLO-pass steps @ c=64. RPS cells include (MiB / CPU%) at that step.

pwsh tools/RpsLoadProbe/run-rps.ps1 -Mode compare-editions
pwsh tools/RpsLoadProbe/validate-edition-gates.ps1 -CsvPath tools/RpsLoadProbe/results/rps-ramp-*.csv
Arm Win sustain Win peak Linux sustain Linux peak Win÷ Lin÷ Gate
twp-cli-reverse-http1 vs library 32214
(118 MiB / 43.4% CPU)
32219 47734
(151 MiB / 49.0% CPU)
47844 1.02× 1.04× ≥ 0.80×
twp-cli-reverse-http1-tls vs library TLS 26495
(138 MiB / 48.3% CPU)
26651 36932
(174 MiB / 48.5% CPU)
37200 1.01× 1.00× ≥ 0.80×
twp-cli-reverse-http1-route vs CLI 32394
(120 MiB / 47.7% CPU)
32428 47369
(149 MiB / 49.2% CPU)
47398 1.01× 0.99× ≥ 0.90×
twp-cli-plus-base-http1 vs CLI 32475
(123 MiB / 46.0% CPU)
32695 47322
(154 MiB / 49.8% CPU)
47744 1.01× 0.99× ≥ 0.90×
twp-cli-plus-cache-http1 (cold) vs CLI 34029
(118 MiB / 64.5% CPU)
34460 49409
(148 MiB / 62.3% CPU)
50104 1.06× 1.04× ≥ 0.70×
twp-cli-intercept-http1 vs CLI 25219
(124 MiB / 54.0% CPU)
25377 35969
(155 MiB / 51.2% CPU)
35977 0.78× 0.75× ≥ 0.70×
twp-cli-plus-waf-http1 vs CLI 31653
(124 MiB / 46.8% CPU)
31837 46367
(151 MiB / 49.6% CPU)
46685 0.98× 0.97× ≥ 0.80×
twp-cli-plus-cidr-http1 vs CLI 31730
(123 MiB / 51.1% CPU)
31757 46828
(150 MiB / 50.2% CPU)
47093 0.98× 0.98× ≥ 0.80×
twp-cli-plus-jwt-http1 vs CLI 30326
(138 MiB / 46.6% CPU)
30386 43906
(179 MiB / 49.5% CPU)
44433 0.94× 0.92× ≥ 0.70×
twp-cli-plus-ratelimit-http1 vs CLI 31713
(123 MiB / 48.8% CPU)
31944 46435
(151 MiB / 49.4% CPU)
46860 0.98× 0.97× ≥ 0.80×
twp-cli-plus-resilience-http1 vs CLI 32373
(128 MiB / 50.5% CPU)
32416 47302
(155 MiB / 49.5% CPU)
47654 1.00× 0.99× ≥ 0.85×
twp-cli-plus-discovery-file-http1 vs CLI 32209
(121 MiB / 46.5% CPU)
32332 47485
(151 MiB / 49.1% CPU)
47770 1.00× 0.99× ≥ 0.80×
twp-cli-plus-metrics-scrape-http1 vs CLI 32192
(126 MiB / 44.8% CPU)
32427 47348
(159 MiB / 49.5% CPU)
48665 1.00× 0.99× ≥ 0.80×
twp-cli-plus-cache-hit-http1 vs cache cold 34015
(117 MiB / 66.0% CPU)
34022 49153
(149 MiB / 63.1% CPU)
49358 1.00× 0.99× ≥ 0.90×
twp-cli-static-http1 vs CLI 53078
(109 MiB / 52.1% CPU)
53980 78333
(150 MiB / 48.4% CPU)
82955 1.65× 1.64× ≥ 0.85×
twp-cli-logging-http1 vs CLI 32440
(119 MiB / 47.4% CPU)
32581 47624
(150 MiB / 49.3% CPU)
48604 1.01× 1.00× ≥ 0.90×
twp-cli-lb-leasttime-http1 vs route 29649
(134 MiB / 54.7% CPU)
30386 44790
(180 MiB / 50.6% CPU)
44825 0.92× 0.95× ≥ 0.85×
twp-cli-dialect-twp-http1 vs CLI 32461
(114 MiB / 49.7% CPU)
32780 47898
(145 MiB / 49.3% CPU)
48154 1.01× 1.00× ≥ 0.90×

validate-edition-gates.ps1 passed on both OS for this run. Library baselines @ c=64 (same job): Win H1 31561 / TLS 26128; Linux H1 45818 / TLS 36869. Laptop smoke ratios stay on Performance Local Lab — Editions.

Cross-version (7.0 vs 6.0)

Gate 2 prerequisite before the v7.0.0 tag: run compare-cross-version (reverse matrix, routes unset) on develop and compare against committed 6.0 baselines from GHA 33087088466 (tools/RpsLoadProbe/results/baseline-6.0-win.csv / baseline-6.0-linux.csv).

Gate Threshold
Peer-normalized RPS (when YARP peer exists) (TWP÷YARP)_7 ÷ (TWP÷YARP)_6 ≥ 0.90 or current (TWP÷YARP) ≥ 0.90
Absolute RPS floor (TWP arms) 7.0 ÷ 6.0 ≥ 0.70 (runner heat — peers move with the box)
RSS 7.0 ÷ 6.0 ≤ 1.20 per TWP arm @ c=64

nginx/YARP absolute RPS is not gated. See validate-cross-version.ps1.

pwsh tools/RpsLoadProbe/run-rps.ps1 -Mode compare-cross-version
pwsh tools/RpsLoadProbe/validate-cross-version.ps1 `
  -BaselineCsv tools/RpsLoadProbe/results/baseline-6.0-win.csv `
  -CurrentCsv  tools/RpsLoadProbe/results/rps-ramp-*.csv

Median of 3 @ 0ef6d4dd. Source: Actions 33270571908. Sustain RPS @ c=64 (SLO-pass median). Absolute 7.0÷6.0 ≈ 0.96–1.00× on same-protocol arms; peer-norm ≈ 0.90–1.03×.

Arm Win 6.0 Win 7.0 Win÷ Linux 6.0 Linux 7.0 Lin÷
twp-reverse-http1 32525 32213 0.99× 32755 31932 0.97×
twp-reverse-http1-tls 26461 26570 1.00× 22784 22351 0.98×
twp-reverse-http2 76038 75316 0.99× 47690 46678 0.98×
twp-reverse-http2-cleartext 40347 40236 1.00× 34556 33316 0.96×
twp-reverse-http3 17529 17380 0.99× 19468 19423 1.00×
twp-reverse-http3-cleartext 19956 19529 0.98× 20395 20070 0.98×
yarp-reverse-http1 (peer) 27504 27254 0.99× 27713 27102 0.98×
yarp-reverse-http2 (peer) 35553 35189 0.99× 29111 28733 0.99×

validate-cross-version.ps1 passed on both OS CSVs (RSS floor 1.20; peer-norm also passes when current TWP÷YARP ≥ 0.90). MITM arms live in compare-product / compare-mitm, not this reverse-only matrix.

Heavier reverse workloads

Separate from the tiny-GET matrix. Same measurement environments. Modes: compare-bodies, compare-post, compare-lossy, compare-tls-cost, compare-arch in RpsLoadProbe. Dispatch each independently via workflow_dispatch (no need to re-run full compare-product). PUT with the same body is the same proxy work as POST; DELETE with no body matches GET — only POST is published. Bodies/POST/lossy stay half-duplex. compare-arch is the slow-consumer / early-response / duplex set. Laptop numbers are on Performance Local Lab; CI medians go in the tables below.

Lossy link = userspace shim (not kernel netem): TCP gets per-buffer delay + occasional whole-connection stalls (honest HOL for multiplexed H2); UDP gets per-datagram delay + drops (QUIC). compare-lossy publishes H1/H2/H3; H3 is where the protocol design is supposed to matter.

Windows — heavier reverse GET (64 KiB / 256 KiB)

Median of 3 repeats on windows-latest @ 024bd68d. Source: Actions 34126816180 (compare-bodies). Warmup 2s / measure 8s. RPS cells include (MiB / CPU%) footprints.

Body Client Origin TWP sustain TWP peak nginx sustain nginx peak YARP sustain YARP peak
64 KiB HTTP/1 · TLS HTTP/1 · plain 🥇 8,810
(113 MiB / 47.9% CPU)
8,810
(113 MiB / 47.9% CPU)
548
(139 MiB / 24.7% CPU)
548
(139 MiB / 24.7% CPU)
7,794
(135 MiB / 45.9% CPU)
7,794
(135 MiB / 45.9% CPU)
64 KiB HTTP/2 · TLS HTTP/1 · plain 🥇 7,762
(178 MiB / 42.6% CPU)
7,762
(178 MiB / 42.6% CPU)
555
(140 MiB / 24.8% CPU)
555
(140 MiB / 24.8% CPU)
6,825
(132 MiB / 49.6% CPU)
6,825
(132 MiB / 49.6% CPU)
64 KiB HTTP/3 · QUIC HTTP/1 · plain 🥇 4,020
(135 MiB / 38.1% CPU)
4,020
(135 MiB / 38.1% CPU)
Not possible (no QUIC) Not possible (no QUIC) 3,784
(193 MiB / 48.1% CPU)
3,784
(193 MiB / 48.1% CPU)
256 KiB HTTP/1 · TLS HTTP/1 · plain 🥇 2,842
(130 MiB / 46.4% CPU)
2,842
(130 MiB / 46.4% CPU)
0
(141 MiB / 24.8% CPU)
162
(141 MiB / 24.8% CPU)
2,600
(134 MiB / 45.6% CPU)
2,600
(134 MiB / 45.6% CPU)
256 KiB HTTP/2 · TLS HTTP/1 · plain 🥇 2,541
(147 MiB / 40.6% CPU)
2,541
(147 MiB / 40.6% CPU)
0
(140 MiB / 24.8% CPU)
148
(140 MiB / 24.8% CPU)
1,911
(134 MiB / 43.7% CPU)
1,911
(134 MiB / 43.7% CPU)
256 KiB HTTP/3 · QUIC HTTP/1 · plain 🥇 1,156
(108 MiB / 39.7% CPU)
1,156
(108 MiB / 39.7% CPU)
Not possible (no QUIC) Not possible (no QUIC) 1,077
(170 MiB / 44.0% CPU)
1,077
(170 MiB / 44.0% CPU)

nginx/Windows collapses on large reverse bodies in this harness; treat as same-OS only. H1 TLS 64 KiB ≈ 1.11× YARP; 256 KiB ≈ 1.23×. H2→H1 64 KiB ≈ 1.21×; H3→H1 64 KiB ≈ 1.18×.

Linux — heavier reverse GET (64 KiB / 256 KiB)

Median of 3 repeats @ 024bd68d. Source: Actions 34126816180 (compare-bodies). Warmup 2s / measure 8s.

Body Client Origin TWP sustain TWP peak nginx sustain nginx peak YARP sustain YARP peak
64 KiB HTTP/1 · TLS HTTP/1 · plain 🥇 7,909
(169 MiB / 44.8% CPU)
7,909
(169 MiB / 44.8% CPU)
5,528
(97 MiB / 51.9% CPU)
5,528
(97 MiB / 51.9% CPU)
6,447
(167 MiB / 48.4% CPU)
6,447
(167 MiB / 48.4% CPU)
64 KiB HTTP/2 · TLS HTTP/1 · plain 🥇 5,792
(235 MiB / 38.6% CPU)
5,792
(235 MiB / 38.6% CPU)
1,554
(98 MiB / 18.2% CPU)
1,554
(98 MiB / 18.2% CPU)
4,814
(158 MiB / 46.8% CPU)
4,814
(158 MiB / 46.8% CPU)
64 KiB HTTP/3 · QUIC HTTP/1 · plain 🥇 5,478
(181 MiB / 44.1% CPU)
5,478
(181 MiB / 44.1% CPU)
0
(124 MiB / 21.4% CPU)
1,696
(124 MiB / 21.4% CPU)
4,331
(221 MiB / 52.3% CPU)
4,331
(221 MiB / 52.3% CPU)
256 KiB HTTP/1 · TLS HTTP/1 · plain 🥇 2,688
(126 MiB / 37.2% CPU)
2,688
(126 MiB / 37.2% CPU)
1,717
(97 MiB / 54.0% CPU)
1,717
(97 MiB / 54.0% CPU)
2,139
(168 MiB / 45.7% CPU)
2,139
(168 MiB / 45.7% CPU)
256 KiB HTTP/2 · TLS HTTP/1 · plain 🥇 1,533
(183 MiB / 32.6% CPU)
1,533
(183 MiB / 32.6% CPU)
578
(98 MiB / 23.1% CPU)
578
(98 MiB / 23.1% CPU)
1,381
(161 MiB / 46.0% CPU)
1,381
(161 MiB / 46.0% CPU)
256 KiB HTTP/3 · QUIC HTTP/1 · plain 🥇 1,458
(148 MiB / 43.5% CPU)
1,458
(148 MiB / 43.5% CPU)
0
(119 MiB / 20.5% CPU)
125
(119 MiB / 20.5% CPU)
1,248
(219 MiB / 48.1% CPU)
1,248
(219 MiB / 48.1% CPU)

On this GHA pass TWP÷YARP H1 TLS ≈ 1.23× (64 KiB) / 1.28× (256 KiB); H2→H1 ≈ 1.23× / 1.16×; H3→H1 ≈ 1.27× / 1.13×. TWP÷nginx H1 TLS ≈ 1.43 / 1.58. Absolute RPS swings by VM; prefer ratios.

Windows — POST 64 KiB request + 64 KiB response

Median of 3 repeats on windows-latest @ 024bd68d. Source: Actions 34126819267 (compare-post).

Client Origin TWP sustain TWP peak nginx sustain nginx peak YARP sustain YARP peak
HTTP/1 · TLS HTTP/1 · plain 🥇 6,578
(102 MiB / 44.2% CPU)
6,578
(102 MiB / 44.2% CPU)
381
(139 MiB / 24.9% CPU)
381
(139 MiB / 24.9% CPU)
4,448
(133 MiB / 56.5% CPU)
4,448
(133 MiB / 56.5% CPU)
HTTP/2 · TLS HTTP/1 · plain 🥇 4,521
(178 MiB / 48.8% CPU)
4,521
(178 MiB / 48.8% CPU)
370
(142 MiB / 24.8% CPU)
370
(142 MiB / 24.8% CPU)
3,851
(134 MiB / 51.9% CPU)
3,851
(134 MiB / 51.9% CPU)
HTTP/3 · QUIC HTTP/1 · plain 🥇 2,211
(177 MiB / 41.2% CPU)
2,211
(177 MiB / 41.2% CPU)
Not possible (no QUIC) Not possible (no QUIC) 2,182
(215 MiB / 48.9% CPU)
2,182
(215 MiB / 48.9% CPU)

TWP leads H1 POST (1.5× YARP), H2 POST (1.2× YARP), and H3 POST (~1.1× YARP).

Linux — POST 64 KiB request + 64 KiB response

Median of 3 repeats @ 024bd68d. Source: Actions 34126819267 (compare-post).

Client Origin TWP sustain TWP peak nginx sustain nginx peak YARP sustain YARP peak
HTTP/1 · TLS HTTP/1 · plain 🥇 7,829
(131 MiB / 40.9% CPU)
7,829
(131 MiB / 40.9% CPU)
5,642
(101 MiB / 39.5% CPU)
5,642
(101 MiB / 39.5% CPU)
5,323
(168 MiB / 53.5% CPU)
5,323
(168 MiB / 53.5% CPU)
HTTP/2 · TLS HTTP/1 · plain 🥇 4,898
(233 MiB / 43.8% CPU)
4,898
(233 MiB / 43.8% CPU)
2,242
(115 MiB / 17.1% CPU)
2,242
(115 MiB / 17.1% CPU)
3,998
(169 MiB / 49.7% CPU)
3,998
(169 MiB / 49.7% CPU)
HTTP/3 · QUIC HTTP/1 · plain 🥇 3,979
(237 MiB / 44.7% CPU)
3,979
(237 MiB / 44.7% CPU)
985
(114 MiB / 23.9% CPU)
985
(114 MiB / 23.9% CPU)
3,302
(261 MiB / 50.9% CPU)
3,302
(261 MiB / 50.9% CPU)

Linux nginx H1/H2/H3 POST completed (nginx.org mainline). TWP÷YARP H1 ≈ 1.5×; H2 ≈ 1.3×; H3 ≈ 1.1×. TWP÷nginx H3 ≈ 4×.

Windows — lossy / high-RTT (H2 HOL / H3 loss)

Userspace 5 ms one-way delay + 1% TCP connection stall (H1/H2) or UDP datagram drop (H3); 64 KiB GET. Median of 3 repeats on windows-latest @ 024bd68d — 34126822092 (compare-lossy).

Client Origin TWP sustain TWP peak nginx sustain nginx peak YARP sustain YARP peak
HTTP/1 · TLS HTTP/1 · plain 🥇 662
(106 MiB / 5.1% CPU)
662
(106 MiB / 5.1% CPU)
639
(138 MiB / 19.3% CPU)
639
(138 MiB / 19.3% CPU)
659
(111 MiB / 5.9% CPU)
659
(111 MiB / 5.9% CPU)
HTTP/2 · TLS HTTP/1 · plain 0
(123 MiB / 1.9% CPU)
85
(123 MiB / 1.9% CPU)
0
(140 MiB / 0.8% CPU)
17
(140 MiB / 0.8% CPU)
0
(99 MiB / 1.1% CPU)
16
(99 MiB / 1.1% CPU)
HTTP/3 · QUIC HTTP/1 · plain 0
(66 MiB / 0.0% CPU)
0
(66 MiB / 0.0% CPU)
Not possible (no QUIC) Not possible (no QUIC) 0
(79 MiB / 0.0% CPU)
0
(79 MiB / 0.0% CPU)

TWP H2 HOL leads (~3.31× YARP). H3 is the protocol-shape win vs H2 HOL on the same lossy session; Win H3 GHA remains 0 (laptop remeasure kept above).

Linux — lossy / high-RTT (H2 HOL / H3 loss)

Median of 3 repeats @ 024bd68d. Source: 34126822092 (compare-lossy; lossy H3 uses quic-http3).

Client Origin TWP sustain TWP peak nginx sustain nginx peak YARP sustain YARP peak
HTTP/1 · TLS HTTP/1 · plain 1,203
(144 MiB / 13.2% CPU)
1,203
(144 MiB / 13.2% CPU)
🥇 1,208
(98 MiB / 11.3% CPU)
1,208
(98 MiB / 11.3% CPU)
1,196
(150 MiB / 16.5% CPU)
1,196
(150 MiB / 16.5% CPU)
HTTP/2 · TLS HTTP/1 · plain 🥇 313
(170 MiB / 6.9% CPU)
313
(170 MiB / 6.9% CPU)
40
(97 MiB / 0.3% CPU)
40
(97 MiB / 0.3% CPU)
40
(127 MiB / 1.4% CPU)
40
(127 MiB / 1.4% CPU)
HTTP/3 · QUIC HTTP/1 · plain 324
(149 MiB / 12.9% CPU)
324
(149 MiB / 12.9% CPU)
90
(107 MiB / 2.4% CPU)
90
(107 MiB / 2.4% CPU)
🥇 357
(182 MiB / 20.8% CPU)
357
(182 MiB / 20.8% CPU)

TWP H2 HOL ≫ YARP (~7.7×). H3 TWP÷YARP ≈ 1×.

Architecture-sensitive

compare-arch isolates slow app readers, origin-early response, H2 duplex, and WebSocket echo. See TWP vs YARP IO model. Laptop 1-rep numbers are on Performance Local Lab.

Median of 3 repeats on matched 4 vCPU / 16 GiB runners @ 024bd68d (34126829362) (compare-arch). Slow consumer = 256 KiB GET, 16 KiB read + 8 ms sleep. Early response = 64 KiB POST, origin writes after 8 KiB. Duplex H2 = overlapping 64 KiB POST on H2 TLS↔H2 TLS. WebSocket = echo round-trips/sec.

compare-lossy (slow network) is already published above; it is not a slow app reader.

Windows

Scenario Client Origin TWP sustain TWP peak nginx sustain nginx peak YARP sustain YARP peak
Slow consumer (256 KiB GET, throttled client read) HTTP/1 · TLS HTTP/1 · plain 🥇 248
(98 MiB / 4.0% CPU)
248
(98 MiB / 4.0% CPU)
208
(141 MiB / 24.8% CPU)
208
(141 MiB / 24.8% CPU)
248
(108 MiB / 4.0% CPU)
248
(108 MiB / 4.0% CPU)
Slow consumer (256 KiB GET, throttled client read) HTTP/2 · TLS HTTP/1 · plain 248
(115 MiB / 4.1% CPU)
248
(115 MiB / 4.1% CPU)
171
(139 MiB / 24.8% CPU)
171
(139 MiB / 24.8% CPU)
🥇 249
(111 MiB / 6.2% CPU)
249
(111 MiB / 6.2% CPU)
Slow consumer (256 KiB GET, throttled client read) HTTP/3 · QUIC HTTP/1 · plain 🥇 264
(94 MiB / 18.1% CPU)
264
(94 MiB / 18.1% CPU)
Not possible (no QUIC) Not possible (no QUIC) 263
(158 MiB / 20.7% CPU)
263
(158 MiB / 20.7% CPU)
Early response (origin writes after first request chunk) HTTP/1 · TLS HTTP/1 · plain 🥇 6,099
(99 MiB / 43.0% CPU)
6,099
(99 MiB / 43.0% CPU)
376
(140 MiB / 24.8% CPU)
376
(140 MiB / 24.8% CPU)
4,216
(136 MiB / 51.4% CPU)
4,216
(136 MiB / 51.4% CPU)
Early response (origin writes after first request chunk) HTTP/2 · TLS HTTP/1 · plain 🥇 4,285
(178 MiB / 35.9% CPU)
4,285
(178 MiB / 35.9% CPU)
0
(141 MiB / 24.6% CPU)
284
(141 MiB / 24.6% CPU)
2,821
(135 MiB / 40.1% CPU)
2,821
(135 MiB / 40.1% CPU)
Early response (origin writes after first request chunk) HTTP/3 · QUIC HTTP/1 · plain 🥇 2,252
(148 MiB / 41.0% CPU)
2,252
(148 MiB / 41.0% CPU)
Not possible (no QUIC) Not possible (no QUIC) 1,721
(197 MiB / 45.2% CPU)
1,721
(197 MiB / 45.2% CPU)
Duplex (both directions live) HTTP/2 · TLS HTTP/2 · TLS 0
(92 MiB / 0.1% CPU)
0
(92 MiB / 0.1% CPU)
Not possible Not possible 0
(114 MiB / 0.1% CPU)
0
(114 MiB / 0.1% CPU)
Duplex (WebSocket / extended CONNECT) HTTP/1 · TLS HTTP/1 · plain 🥇 31,275
(98 MiB / 40.2% CPU)
31,275
(98 MiB / 40.2% CPU)
18,071
(141 MiB / 24.6% CPU)
18,071
(141 MiB / 24.6% CPU)
28,913
(88 MiB / 43.6% CPU)
28,913
(88 MiB / 43.6% CPU)

Linux

Scenario Client Origin TWP sustain TWP peak nginx sustain nginx peak YARP sustain YARP peak
Slow consumer (256 KiB GET, throttled client read) HTTP/1 · TLS HTTP/1 · plain 🥇 465
(118 MiB / 10.0% CPU)
465
(118 MiB / 10.0% CPU)
406
(97 MiB / 10.4% CPU)
406
(97 MiB / 10.4% CPU)
418
(147 MiB / 14.9% CPU)
418
(147 MiB / 14.9% CPU)
Slow consumer (256 KiB GET, throttled client read) HTTP/2 · TLS HTTP/1 · plain 🥇 472
(141 MiB / 20.2% CPU)
472
(141 MiB / 20.2% CPU)
259
(97 MiB / 7.7% CPU)
259
(97 MiB / 7.7% CPU)
467
(146 MiB / 25.7% CPU)
467
(146 MiB / 25.7% CPU)
Slow consumer (256 KiB GET, throttled client read) HTTP/3 · QUIC HTTP/1 · plain 🥇 471
(129 MiB / 35.7% CPU)
471
(129 MiB / 35.7% CPU)
0
(118 MiB / 21.4% CPU)
193
(118 MiB / 21.4% CPU)
466
(196 MiB / 40.7% CPU)
466
(196 MiB / 40.7% CPU)
Early response (origin writes after first request chunk) HTTP/1 · TLS HTTP/1 · plain 🥇 4,568
(135 MiB / 47.8% CPU)
4,568
(135 MiB / 47.8% CPU)
3,545
(98 MiB / 49.3% CPU)
3,545
(98 MiB / 49.3% CPU)
3,027
(176 MiB / 56.5% CPU)
3,027
(176 MiB / 56.5% CPU)
Early response (origin writes after first request chunk) HTTP/2 · TLS HTTP/1 · plain 🥇 3,317
(220 MiB / 46.1% CPU)
3,317
(220 MiB / 46.1% CPU)
0
(114 MiB / 23.6% CPU)
1,340
(114 MiB / 23.6% CPU)
2,151
(169 MiB / 48.0% CPU)
2,151
(169 MiB / 48.0% CPU)
Early response (origin writes after first request chunk) HTTP/3 · QUIC HTTP/1 · plain 🥇 2,823
(204 MiB / 45.1% CPU)
2,823
(204 MiB / 45.1% CPU)
0
(114 MiB / 24.6% CPU)
431
(114 MiB / 24.6% CPU)
2,073
(243 MiB / 48.3% CPU)
2,073
(243 MiB / 48.3% CPU)
Duplex (both directions live) HTTP/2 · TLS HTTP/2 · TLS 0
(101 MiB / 0.1% CPU)
0
(101 MiB / 0.1% CPU)
Not possible Not possible 0
(150 MiB / 0.3% CPU)
0
(150 MiB / 0.3% CPU)
Duplex (WebSocket / extended CONNECT) HTTP/1 · TLS HTTP/1 · plain 29,049
(124 MiB / 44.2% CPU)
29,049
(124 MiB / 44.2% CPU)
🥇 33,342
(98 MiB / 36.2% CPU)
33,342
(98 MiB / 36.2% CPU)
26,992
(123 MiB / 44.1% CPU)
26,992
(123 MiB / 44.1% CPU)

Slow consumer is sleep-bound; H1/H2/H3 sit in the same band. Early-response H1/H2/H3: TWP leads (H1 early ≈ 2.00× / 1.47× YARP Win/Linux). Duplex H2: YARP leads by design — Win ≈ 0.59× (1,270 / 2,135), Linux ≈ 0.15× (282 / 1,882); irreducible concurrent-copier cell (see IO model). WebSocket: TWP÷YARP Windows ≈ 1.06×; Linux nginx leads.

TLS termination cost (H1 TLS → cleartext origin)

Isolates keep-alive tiny GET vs new connection per request (handshake-dominated) vs keep-alive 256 KiB. Product comparison uses RPS and end-to-end latency; TWP can also capture ClientTlsTiming when TWP_RPS_CAPTURE_TLS=1 (child process) — nginx/YARP have no equivalent hook.

Windows

Median of 3 repeats on windows-latest @ 024bd68d. Source: Actions 34126826292 (compare-tls-cost). Absolute RPS on GHA swings hard; prefer TWP÷YARP.

Workload TWP sustain TWP peak nginx sustain nginx peak YARP sustain YARP peak
Keep-alive · tiny GET 🥇 20,329
(84 MiB / 47.8% CPU)
20,329
(84 MiB / 47.8% CPU)
8,644
(138 MiB / 24.8% CPU)
8,644
(138 MiB / 24.8% CPU)
17,804
(102 MiB / 50.3% CPU)
17,804
(102 MiB / 50.3% CPU)
New-connection · tiny GET 🥇 712
(85 MiB / 9.2% CPU)
712
(85 MiB / 9.2% CPU)
0
(138 MiB / 24.0% CPU)
240
(138 MiB / 24.0% CPU)
706
(110 MiB / 11.1% CPU)
706
(110 MiB / 11.1% CPU)
Keep-alive · 256 KiB GET 🥇 2,839
(118 MiB / 44.5% CPU)
2,839
(118 MiB / 44.5% CPU)
0
(141 MiB / 24.6% CPU)
156
(141 MiB / 24.6% CPU)
2,609
(137 MiB / 46.5% CPU)
2,609
(137 MiB / 46.5% CPU)

Linux

Median of 3 repeats @ 024bd68d. Source: Actions 34126826292 (compare-tls-cost).

Workload TWP sustain TWP peak nginx sustain nginx peak YARP sustain YARP peak
Keep-alive · tiny GET 23,672
(105 MiB / 49.9% CPU)
23,672
(105 MiB / 49.9% CPU)
🥇 27,804
(98 MiB / 41.6% CPU)
27,804
(98 MiB / 41.6% CPU)
20,362
(130 MiB / 50.1% CPU)
20,362
(130 MiB / 50.1% CPU)
New-connection · tiny GET 974
(121 MiB / 47.1% CPU)
974
(121 MiB / 47.1% CPU)
🥇 1,007
(98 MiB / 44.3% CPU)
1,007
(98 MiB / 44.3% CPU)
964
(149 MiB / 46.2% CPU)
964
(149 MiB / 46.2% CPU)
Keep-alive · 256 KiB GET 🥇 2,722
(126 MiB / 37.0% CPU)
2,722
(126 MiB / 37.0% CPU)
1,728
(97 MiB / 53.4% CPU)
1,728
(97 MiB / 53.4% CPU)
2,152
(169 MiB / 45.9% CPU)
2,152
(169 MiB / 45.9% CPU)

All three workloads are >1.00× YARP on both OS. nginx leads Linux keep-alive tiny and Linux new-connection; TWP is second, YARP third.

Other measurements

What Result
HTTPS TTFB vs direct (median, 14 hosts) Cold ≈ parity (−1 ms); warm −25 ms (proxy faster)
HTTP/1 loopback GET (no body intercept) ~128 µs, ~9.9 KB allocated / request
Basic example footprint (Release, after load) ~74 MB working set · ~24–29 MB private bytes
dotnet run -c Release --project benchmarks/Titanium.Web.Proxy.Benchmarks -- --filter '*Throughput*'

Local laptop BDN @ 9d7c2966 (Release):

Benchmark Setup Mean Allocated / op
HTTP/1 GET through proxy Passthrough 128 µs 9.9 KB
HTTP/2 multiplexed GETs 10 concurrent streams 253 µs / batch ~4.4 KB / request

Raising limits on large hosts

There is no artificial upper clamp on server defaults. Per-endpoint overrides:

Knob Scope Default Override
ProxyServer.MaxCachedConnections process, per upstream host 128 any ≥ 1
ProxyEndPoint.MaxCachedConnections endpoint → pool depth for that EP’s sessions null (use server) e.g. 256 on reverse EP
ProxyEndPoint.MaxConcurrentClients endpoint admission null (off) any ≥ 1
ResourceLimits.MaxConcurrentStreamsPerConnection H2 streams 256 ProxyResourceLimits.Create(...)
TransparentQuicProxyEndPoint.MaxInboundBidirectionalStreams H3 100 (probe uses 256) property on EP
ForwardCleartext transparent TLS terminate false true + decrypt
proxy.MaxCachedConnections = 512;
proxy.ResourceLimits = ProxyResourceLimits.Create(
    /* … */,
    maxConcurrentStreamsPerConnection: 1000,
    maxCachedConnectionsPerHost: 512,
    /* … */);

var ep = new TransparentProxyEndPoint(IPAddress.Any, 443, decryptSsl: true)
{
    ForwardHost = "127.0.0.1",
    ForwardPort = 8080,
    ForwardCleartext = true,
    MaxCachedConnections = 256,
    GenericCertificateName = "example.com"
};
ep.BeforeSslAuthenticate += (_, a) =>
{
    a.UpstreamHttpProtocol = UpstreamHttpProtocol.Http11;
    a.AllowHttpProtocolTranslation = true; // HTTP/2 client → HTTP/1 origin
    return Task.CompletedTask;
};

Clone this wiki locally