Repository navigation
Performance
Titanium targets low-overhead MITM proxying: connection pooling, HTTP/2 multiplexing, and buffer reuse. Numbers below are Release measurements with RpsLoadProbe (and BenchmarkDotNet / Basic example where noted). Publishable tables cite GitHub Actions medians on matched 4 vCPU / 16 GiB runners. Absolute RPS still varies by OS kernel, TLS, and MsQuic packaging — compare within a table, not across Windows vs Linux.
Control arms: nginx (native C reverse-proxy ceiling; Linux is authoritative) and YARP (Yarp.ReverseProxy, managed .NET reverse proxy). Neither can MITM (no CONNECT / forged certs). 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.
- Measurement environment
- Windows — Titanium vs nginx vs YARP
- Linux — Titanium vs nginx vs YARP
-
Heavier reverse workloads
- Windows — heavier reverse GET (64 KiB / 256 KiB)
- Linux — heavier reverse GET (64 KiB / 256 KiB)
- Windows — POST 64 KiB request + 64 KiB response
- Linux — POST 64 KiB request + 64 KiB response
- Windows — lossy / high-RTT (H2 HOL / H3 loss)
- Linux — lossy / high-RTT (H2 HOL / H3 loss)
- Architecture-sensitive
- TLS termination cost (H1 TLS → cleartext origin)
- Other measurements
- Raising limits on large hosts
Both OS use the standard public-repo GitHub-hosted runner class (4 vCPU / 16 GiB / 14 GB SSD). Same harness knobs (workflow_dispatch RPS saturation: warmup 2s / measure 8s; concurrency 8, 16, 32, 64; median of 3 repeats). 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.
| 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 |
| 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 |
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 @ 9d7c2966 — 32866709227. 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). H3→H1 (saturation Block C): Win TWP RSS 103 MiB vs YARP 119 (0.87×); Linux 142 vs 181 (0.78×). RPS vs YARP (0.99× / 1.1×).
pwsh tools/RpsLoadProbe/run-rps.ps1 -Mode compare-saturation| 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,717 (53 MiB / 41.6% CPU) |
50,717 (53 MiB / 41.6% CPU) |
100.0% |
| origin-direct-bombardier | bombardier |
38,335 (55 MiB / 22.7% CPU) |
38,335 (55 MiB / 22.7% CPU) |
75.6% |
| bare-reverse-http1 | dotnet-httpclient |
25,467 (55 MiB / 46.5% CPU) |
25,467 (55 MiB / 46.5% CPU) |
50.2% |
| nginx-reverse-http1 | dotnet-httpclient |
13,259 (121 MiB / 25.0% CPU) |
13,422 (121 MiB / 25.0% CPU) |
26.5% |
| yarp-reverse-http1 | dotnet-httpclient |
21,331 (89 MiB / 49.9% CPU) |
21,331 (89 MiB / 49.9% CPU) |
42.1% |
| twp-reverse-http1 | dotnet-httpclient | 🥇 25,358 (75 MiB / 49.2% CPU) |
25,358 (75 MiB / 49.2% CPU) |
50.0% |
Linux (ubuntu-latest)
| Arm | Generator | Sustain | Peak | % of origin-HttpClient |
|---|---|---|---|---|
| origin-direct | dotnet-httpclient |
68,992 (78 MiB / 41.8% CPU) |
68,992 (78 MiB / 41.8% CPU) |
100.0% |
| origin-direct-bombardier | bombardier |
41,996 (78 MiB / 34.4% CPU) |
41,996 (78 MiB / 34.4% CPU) |
60.9% |
| bare-reverse-http1 | dotnet-httpclient |
32,077 (60 MiB / 45.0% CPU) |
32,077 (60 MiB / 45.0% CPU) |
46.5% |
| nginx-reverse-http1 | dotnet-httpclient | 🥇 38,039 (72 MiB / 40.9% CPU) |
38,039 (72 MiB / 40.9% CPU) |
55.1% |
| yarp-reverse-http1 | dotnet-httpclient |
26,736 (113 MiB / 49.2% CPU) |
26,736 (113 MiB / 49.2% CPU) |
38.8% |
| twp-reverse-http1 | dotnet-httpclient |
31,431 (84 MiB / 50.0% CPU) |
31,431 (84 MiB / 50.0% CPU) |
45.6% |
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).
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 |
8,264 (248 MiB / 24.8% CPU) |
8,411 (248 MiB / 24.8% CPU) |
0.29× | 1× |
| yarp-reverse-http2 | dotnet-httpclient |
29,096 (95 MiB / 53.0% CPU) |
29,096 (95 MiB / 53.0% CPU) |
1× | 3.46× |
| twp-reverse-http2-cleartext | dotnet-httpclient | 🥇 35,180 (96 MiB / 52.0% CPU) |
35,180 (96 MiB / 52.0% CPU) |
1.21× | 4.18× |
Linux (ubuntu-latest)
| Arm | Generator | Sustain | Peak | ÷YARP | ÷nginx |
|---|---|---|---|---|---|
| nginx-reverse-http2 | dotnet-httpclient |
14,490 (97 MiB / 19.4% CPU) |
14,490 (97 MiB / 19.4% CPU) |
0.51× | 1× |
| yarp-reverse-http2 | dotnet-httpclient |
28,448 (120 MiB / 49.0% CPU) |
28,448 (120 MiB / 49.0% CPU) |
1× | 1.96× |
| twp-reverse-http2-cleartext | dotnet-httpclient | 🥇 33,751 (120 MiB / 51.7% CPU) |
33,751 (120 MiB / 51.7% CPU) |
1.19× | 2.33× |
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 | — | — |
| yarp-reverse-http3-cleartext | dotnet-httpclient |
14,549 (119 MiB / 50.8% CPU) |
15,166 (119 MiB / 50.8% CPU) |
1× | — |
| twp-reverse-http3-cleartext | dotnet-httpclient | 🥇 15,028 (103 MiB / 46.5% CPU) |
15,028 (103 MiB / 46.5% CPU) |
0.99× | — |
Linux (ubuntu-latest)
| Arm | Generator | Sustain | Peak | ÷YARP | ÷nginx |
|---|---|---|---|---|---|
| nginx-reverse-http3-cleartext | dotnet-httpclient |
0 (103 MiB / 22.4% CPU) |
15,186 (103 MiB / 22.4% CPU) |
0.85× | 1× |
| yarp-reverse-http3-cleartext | dotnet-httpclient |
17,856 (181 MiB / 49.5% CPU) |
17,856 (181 MiB / 49.5% CPU) |
1× | 1.18× |
| twp-reverse-http3-cleartext | dotnet-httpclient | 🥇 20,378 (142 MiB / 50.4% CPU) |
20,378 (142 MiB / 50.4% CPU) |
1.14× | 1.34× |
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 may reuse reverse relay); Full = mutating handlers (x-twp-rps-probe) forcing 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-productjob. - Not possible = product cannot do that path. Not measured = path exists but no published number yet for that OS.
- Product refresh:
compare-product@c71937a2— 32960766249. Heavier/saturation/tls: 32866709227, 32871900682, 32866714851, 32866717729, 32866720742, 32866723562.
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-saturationClient / origin: HTTP version and whether TLS is used (plain = cleartext, TLS = encrypted, QUIC = HTTP/3).
Median of 3 repeats on windows-latest (4 vCPU / 16 GiB). Bare reverse 5×5 @ c71937a2 — compare-product 32960766249. 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).
| Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | YARP sustain | YARP peak |
|---|---|---|---|---|---|---|---|
| HTTP/1 · plain | HTTP/1 · plain | 🥇 41025 (72 MiB / 48.1% CPU) |
🥇 41025 (72 MiB / 48.1% CPU) |
25024 (121 MiB / 24.8% CPU) |
25024 (121 MiB / 24.8% CPU) |
34609 (86 MiB / 48.4% CPU) |
34609 (86 MiB / 48.4% CPU) |
| HTTP/1 · plain | HTTP/1 · TLS | 🥇 34712 (81 MiB / 50.5% CPU) |
🥇 34712 (81 MiB / 50.5% CPU) |
Not possible | Not possible |
30528 (99 MiB / 50.8% CPU) |
30528 (99 MiB / 50.8% CPU) |
| HTTP/1 · plain | HTTP/2 · plain | 🥇 55322 (101 MiB / 47.8% CPU) |
🥇 55322 (101 MiB / 47.8% CPU) |
Not possible | Not possible |
52232 (92 MiB / 51.1% CPU) |
52232 (92 MiB / 51.1% CPU) |
| HTTP/1 · plain | HTTP/2 · TLS | 🥇 49471 (107 MiB / 48.5% CPU) |
🥇 49471 (107 MiB / 48.5% CPU) |
Not possible | Not possible |
46990 (100 MiB / 49.7% CPU) |
46990 (100 MiB / 49.7% CPU) |
| HTTP/1 · plain | HTTP/3 · QUIC | 🥇 27928 (112 MiB / 50.8% CPU) |
🥇 27928 (112 MiB / 50.8% CPU) |
Not possible (no QUIC) | Not possible (no QUIC) |
27476 (124 MiB / 51.6% CPU) |
27476 (124 MiB / 51.6% CPU) |
| HTTP/1 · TLS | HTTP/1 · plain | 🥇 34140 (83 MiB / 43.9% CPU) |
🥇 34140 (83 MiB / 43.9% CPU) |
16331 (137 MiB / 24.8% CPU) |
16519 (137 MiB / 24.8% CPU) |
29749 (101 MiB / 49.0% CPU) |
29749 (101 MiB / 49.0% CPU) |
| HTTP/1 · TLS | HTTP/1 · TLS | 🥇 31041 (83 MiB / 46.6% CPU) |
🥇 31041 (83 MiB / 46.6% CPU) |
Not possible | Not possible |
27018 (101 MiB / 50.0% CPU) |
27018 (101 MiB / 50.0% CPU) |
| HTTP/1 · TLS | HTTP/2 · plain | 🥇 43769 (110 MiB / 47.5% CPU) |
🥇 43769 (110 MiB / 47.5% CPU) |
Not possible | Not possible |
43070 (109 MiB / 49.2% CPU) |
43070 (109 MiB / 49.2% CPU) |
| HTTP/1 · TLS | HTTP/2 · TLS | 🥇 40108 (114 MiB / 47.9% CPU) |
🥇 40108 (114 MiB / 47.9% CPU) |
Not possible | Not possible |
39005 (106 MiB / 48.0% CPU) |
39005 (106 MiB / 48.0% CPU) |
| HTTP/1 · TLS | HTTP/3 · QUIC | 🥇 24269 (113 MiB / 49.6% CPU) |
🥇 24269 (113 MiB / 49.6% CPU) |
Not possible (no QUIC) | Not possible (no QUIC) |
23450 (128 MiB / 50.8% CPU) |
23450 (128 MiB / 50.8% CPU) |
| HTTP/2 · plain | HTTP/1 · plain | 🥇 54074 (90 MiB / 51.9% CPU) |
🥇 54074 (90 MiB / 51.9% CPU) |
Not possible | Not possible |
49807 (85 MiB / 51.8% CPU) |
49807 (85 MiB / 51.8% CPU) |
| HTTP/2 · plain | HTTP/1 · TLS | 🥇 46754 (101 MiB / 52.0% CPU) |
🥇 46754 (101 MiB / 52.0% CPU) |
Not possible | Not possible |
41956 (91 MiB / 50.6% CPU) |
41956 (91 MiB / 50.6% CPU) |
| HTTP/2 · plain | HTTP/2 · plain | 🥇 119261 (72 MiB / 35.1% CPU) |
🥇 119261 (72 MiB / 35.1% CPU) |
Not possible | Not possible |
89148 (97 MiB / 52.7% CPU) |
89148 (97 MiB / 52.7% CPU) |
| HTTP/2 · plain | HTTP/2 · TLS | 🥇 102054 (82 MiB / 37.2% CPU) |
🥇 102054 (82 MiB / 37.2% CPU) |
Not possible | Not possible |
77568 (104 MiB / 47.5% CPU) |
77568 (104 MiB / 47.5% CPU) |
| HTTP/2 · plain | HTTP/3 · QUIC | 🥇 42180 (135 MiB / 53.8% CPU) |
🥇 42180 (135 MiB / 53.8% CPU) |
Not possible (no QUIC) | Not possible (no QUIC) |
40400 (131 MiB / 51.8% CPU) |
40400 (131 MiB / 51.8% CPU) |
| HTTP/2 · TLS | HTTP/1 · plain | 🥇 51831 (100 MiB / 52.7% CPU) |
🥇 51831 (100 MiB / 52.7% CPU) |
14574 (137 MiB / 24.2% CPU) |
14876 (137 MiB / 24.2% CPU) |
45383 (93 MiB / 49.8% CPU) |
45383 (93 MiB / 49.8% CPU) |
| HTTP/2 · TLS | HTTP/1 · TLS | 🥇 45126 (102 MiB / 51.7% CPU) |
🥇 45126 (102 MiB / 51.7% CPU) |
Not possible | Not possible |
39564 (95 MiB / 51.6% CPU) |
39564 (95 MiB / 51.6% CPU) |
| HTTP/2 · TLS | HTTP/2 · plain | 🥇 110147 (90 MiB / 36.2% CPU) |
🥇 110147 (90 MiB / 36.2% CPU) |
Not possible | Not possible |
75865 (101 MiB / 52.3% CPU) |
75865 (101 MiB / 52.3% CPU) |
| HTTP/2 · TLS | HTTP/2 · TLS | 🥇 97618 (87 MiB / 37.5% CPU) |
🥇 97618 (87 MiB / 37.5% CPU) |
Not possible | Not possible |
69005 (101 MiB / 49.6% CPU) |
69005 (101 MiB / 49.6% CPU) |
| HTTP/2 · TLS | HTTP/3 · QUIC | 🥇 40752 (141 MiB / 52.8% CPU) |
🥇 40752 (141 MiB / 52.8% CPU) |
Not possible (no QUIC) | Not possible (no QUIC) |
36652 (132 MiB / 53.1% CPU) |
36652 (132 MiB / 53.1% CPU) |
| HTTP/3 · QUIC | HTTP/1 · plain | 🥇 24291 (112 MiB / 44.5% CPU) |
🥇 24291 (112 MiB / 44.5% CPU) |
Not possible (no QUIC) | Not possible (no QUIC) |
22638 (121 MiB / 48.1% CPU) |
23401 (121 MiB / 48.1% CPU) |
| HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 22674 (119 MiB / 44.6% CPU) |
🥇 22674 (119 MiB / 44.6% CPU) |
Not possible | Not possible |
20885 (119 MiB / 49.9% CPU) |
21058 (119 MiB / 49.9% CPU) |
| HTTP/3 · QUIC | HTTP/2 · plain | 🥇 37142 (129 MiB / 49.4% CPU) |
🥇 37142 (129 MiB / 49.4% CPU) |
Not possible (no H3→H2) | Not possible (no H3→H2) |
34757 (155 MiB / 50.2% CPU) |
34757 (155 MiB / 50.2% CPU) |
| HTTP/3 · QUIC | HTTP/2 · TLS | 🥇 33873 (130 MiB / 47.9% CPU) |
🥇 33873 (130 MiB / 47.9% CPU) |
Not possible (no H3→H2) | Not possible (no H3→H2) |
33573 (163 MiB / 48.2% CPU) |
33573 (163 MiB / 48.2% CPU) |
| HTTP/3 · QUIC | HTTP/3 · QUIC |
20878 (129 MiB / 46.4% CPU) |
20878 (129 MiB / 46.4% CPU) |
Not possible (no QUIC) | Not possible (no QUIC) | 🥇 24536 (173 MiB / 50.8% CPU) |
🥇 24536 (173 MiB / 50.8% CPU) |
Same Client×Origin wires with interception on (compare-product 32960766249). Lite = no-op handlers (unchanged-lite finish). Full = mutating handlers (x-twp-rps-probe) forcing non-lite re-encode. nginx/YARP cannot MITM. Lite÷Reverse / Full÷Reverse vs bare reverse (same job).
Post-fix note (2026-08-26): Phase 2 closed the H3→H1 Full gap: preencoded QPACK relay was gated on empty ViaHeaderPseudonym while the default is "titanium-web-proxy". Cool paired laptop gates (h3h1-spot/, c=64): H3→H1 plain Full÷Reverse 0.91×, H3→H1 TLS 0.995×, H1 plain 0.91× — all ≥0.80. Published table below is still pre-fix CI; remasure via GHA compare-product ×3 is pending.
| Client | Origin | Lite sustain | Full sustain | Lite÷Reverse | Full÷Reverse |
|---|---|---|---|---|---|
| HTTP/1 · plain | HTTP/1 · plain | 🥇 40598 (76 MiB / 48.1% CPU) |
35091 (79 MiB / 52.6% CPU) |
0.99× | 0.86× |
| HTTP/1 · plain | HTTP/1 · TLS | 🥇 34487 (88 MiB / 49.2% CPU) |
30702 (91 MiB / 51.3% CPU) |
0.99× | 0.88× |
| HTTP/1 · plain | HTTP/2 · plain | 🥇 53742 (120 MiB / 50.9% CPU) |
52732 (123 MiB / 50.6% CPU) |
0.97× | 0.95× |
| HTTP/1 · plain | HTTP/2 · TLS | 🥇 47896 (115 MiB / 51.6% CPU) |
48004 (117 MiB / 49.7% CPU) |
0.97× | 0.97× |
| HTTP/1 · plain | HTTP/3 · QUIC | 🥇 27577 (109 MiB / 52.6% CPU) |
27356 (112 MiB / 51.4% CPU) |
0.99× | 0.98× |
| HTTP/1 · TLS | HTTP/1 · plain | 🥇 33374 (87 MiB / 49.9% CPU) |
29388 (87 MiB / 48.4% CPU) |
0.98× | 0.86× |
| HTTP/1 · TLS | HTTP/1 · TLS | 🥇 30543 (91 MiB / 46.3% CPU) |
27441 (90 MiB / 52.5% CPU) |
0.98× | 0.88× |
| HTTP/1 · TLS | HTTP/2 · plain | 🥇 41828 (126 MiB / 50.7% CPU) |
41796 (136 MiB / 48.7% CPU) |
0.96× | 0.95× |
| HTTP/1 · TLS | HTTP/2 · TLS | 🥇 39553 (120 MiB / 49.9% CPU) |
38882 (125 MiB / 51.0% CPU) |
0.99× | 0.97× |
| HTTP/1 · TLS | HTTP/3 · QUIC | 🥇 24173 (109 MiB / 52.1% CPU) |
23744 (111 MiB / 51.9% CPU) |
1.00× | 0.98× |
| HTTP/2 · plain | HTTP/1 · plain | 🥇 52800 (97 MiB / 52.0% CPU) |
51344 (94 MiB / 53.0% CPU) |
0.98× | 0.95× |
| HTTP/2 · plain | HTTP/1 · TLS | 🥇 44671 (107 MiB / 54.1% CPU) |
44315 (103 MiB / 54.3% CPU) |
0.96× | 0.95× |
| HTTP/2 · plain | HTTP/2 · plain | 🥇 101312 (71 MiB / 49.5% CPU) |
18755 (70 MiB / 51.0% CPU) |
0.85× | 0.16× |
| HTTP/2 · plain | HTTP/2 · TLS | 🥇 88479 (89 MiB / 48.2% CPU) |
13680 (79 MiB / 48.5% CPU) |
0.87× | 0.13× |
| HTTP/2 · plain | HTTP/3 · QUIC | 🥇 41372 (132 MiB / 55.0% CPU) |
40516 (129 MiB / 53.9% CPU) |
0.98× | 0.96× |
| HTTP/2 · TLS | HTTP/1 · plain | 🥇 49960 (107 MiB / 50.6% CPU) |
49168 (108 MiB / 52.2% CPU) |
0.96× | 0.95× |
| HTTP/2 · TLS | HTTP/1 · TLS | 🥇 42231 (102 MiB / 52.5% CPU) |
43365 (99 MiB / 53.1% CPU) |
0.94× | 0.96× |
| HTTP/2 · TLS | HTTP/2 · plain | 🥇 96587 (98 MiB / 46.7% CPU) |
19466 (87 MiB / 53.9% CPU) |
0.88× | 0.18× |
| HTTP/2 · TLS | HTTP/2 · TLS | 🥇 85710 (96 MiB / 43.5% CPU) |
14442 (91 MiB / 49.4% CPU) |
0.88× | 0.15× |
| HTTP/2 · TLS | HTTP/3 · QUIC | 🥇 39904 (138 MiB / 53.5% CPU) |
39272 (139 MiB / 54.9% CPU) |
0.98× | 0.96× |
| HTTP/3 · QUIC | HTTP/1 · plain | 🥇 23440 (111 MiB / 45.6% CPU) |
16283 (111 MiB / 49.0% CPU) |
0.96× | 0.67× |
| HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 20759 (120 MiB / 46.9% CPU) |
13849 (99 MiB / 49.8% CPU) |
0.92× | 0.61× |
| HTTP/3 · QUIC | HTTP/2 · plain | 🥇 33821 (138 MiB / 49.2% CPU) |
32620 (140 MiB / 49.6% CPU) |
0.91× | 0.88× |
| HTTP/3 · QUIC | HTTP/2 · TLS | 🥇 31759 (141 MiB / 50.2% CPU) |
31318 (145 MiB / 51.1% CPU) |
0.94× | 0.92× |
| HTTP/3 · QUIC | HTTP/3 · QUIC | 🥇 19614 (126 MiB / 48.9% CPU) |
18634 (111 MiB / 45.7% CPU) |
0.94× | 0.89× |
| HTTP/1 · plain (CONNECT) | HTTP/1 · TLS | 🥇 25522 (106 MiB / 53.6% CPU) |
24557 (103 MiB / 55.8% CPU) |
n/a | n/a |
Median of 3 repeats on ubuntu-latest (4 vCPU / 16 GiB). Bare reverse 5×5 @ c71937a2 — compare-product 32960766249. 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.
| Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | YARP sustain | YARP peak |
|---|---|---|---|---|---|---|---|
| HTTP/1 · plain | HTTP/1 · plain |
32442 (82 MiB / 50.0% CPU) |
32442 (82 MiB / 50.0% CPU) |
🥇 38910 (72 MiB / 40.5% CPU) |
🥇 38910 (72 MiB / 40.5% CPU) |
27468 (115 MiB / 49.9% CPU) |
27468 (115 MiB / 49.9% CPU) |
| HTTP/1 · plain | HTTP/1 · TLS | 🥇 24088 (99 MiB / 50.0% CPU) |
🥇 24088 (99 MiB / 50.0% CPU) |
Not possible | Not possible |
21483 (124 MiB / 50.4% CPU) |
21483 (124 MiB / 50.4% CPU) |
| HTTP/1 · plain | HTTP/2 · plain | 🥇 38601 (129 MiB / 51.5% CPU) |
🥇 38601 (129 MiB / 51.5% CPU) |
Not possible | Not possible |
34502 (125 MiB / 49.5% CPU) |
34502 (125 MiB / 49.5% CPU) |
| HTTP/1 · plain | HTTP/2 · TLS | 🥇 31103 (131 MiB / 49.8% CPU) |
🥇 31103 (131 MiB / 49.8% CPU) |
Not possible | Not possible |
29700 (132 MiB / 48.1% CPU) |
29700 (132 MiB / 48.1% CPU) |
| HTTP/1 · plain | HTTP/3 · QUIC | 🥇 22339 (131 MiB / 51.9% CPU) |
🥇 22339 (131 MiB / 51.9% CPU) |
Not possible (no QUIC) | Not possible (no QUIC) |
21210 (149 MiB / 49.3% CPU) |
21210 (149 MiB / 49.3% CPU) |
| HTTP/1 · TLS | HTTP/1 · plain |
23293 (105 MiB / 49.3% CPU) |
23293 (105 MiB / 49.3% CPU) |
🥇 26873 (97 MiB / 41.5% CPU) |
🥇 26873 (97 MiB / 41.5% CPU) |
19966 (136 MiB / 50.1% CPU) |
19966 (136 MiB / 50.1% CPU) |
| HTTP/1 · TLS | HTTP/1 · TLS | 🥇 19135 (104 MiB / 48.3% CPU) |
🥇 19135 (104 MiB / 48.3% CPU) |
Not possible | Not possible |
17065 (138 MiB / 49.6% CPU) |
17065 (138 MiB / 49.6% CPU) |
| HTTP/1 · TLS | HTTP/2 · plain | 🥇 26310 (142 MiB / 49.8% CPU) |
🥇 26310 (142 MiB / 49.8% CPU) |
Not possible | Not possible |
24764 (144 MiB / 49.1% CPU) |
24764 (144 MiB / 49.1% CPU) |
| HTTP/1 · TLS | HTTP/2 · TLS | 🥇 23637 (146 MiB / 48.0% CPU) |
🥇 23637 (146 MiB / 48.0% CPU) |
Not possible | Not possible |
22212 (145 MiB / 47.2% CPU) |
22212 (145 MiB / 47.2% CPU) |
| HTTP/1 · TLS | HTTP/3 · QUIC | 🥇 17278 (134 MiB / 51.3% CPU) |
🥇 17278 (134 MiB / 51.3% CPU) |
Not possible (no QUIC) | Not possible (no QUIC) |
16348 (157 MiB / 49.5% CPU) |
16348 (157 MiB / 49.5% CPU) |
| HTTP/2 · plain | HTTP/1 · plain | 🥇 37017 (111 MiB / 53.0% CPU) |
🥇 37017 (111 MiB / 53.0% CPU) |
Not possible | Not possible |
34334 (118 MiB / 49.9% CPU) |
34334 (118 MiB / 49.9% CPU) |
| HTTP/2 · plain | HTTP/1 · TLS | 🥇 28470 (126 MiB / 51.8% CPU) |
🥇 28470 (126 MiB / 51.8% CPU) |
Not possible | Not possible |
25800 (124 MiB / 50.2% CPU) |
25800 (124 MiB / 50.2% CPU) |
| HTTP/2 · plain | HTTP/2 · plain | 🥇 64814 (100 MiB / 39.7% CPU) |
🥇 64814 (100 MiB / 39.7% CPU) |
Not possible | Not possible |
48617 (121 MiB / 46.6% CPU) |
48617 (121 MiB / 46.6% CPU) |
| HTTP/2 · plain | HTTP/2 · TLS | 🥇 51516 (109 MiB / 39.4% CPU) |
🥇 51516 (109 MiB / 39.4% CPU) |
Not possible | Not possible |
39280 (124 MiB / 45.8% CPU) |
39280 (124 MiB / 45.8% CPU) |
| HTTP/2 · plain | HTTP/3 · QUIC | 🥇 26375 (140 MiB / 50.0% CPU) |
🥇 26375 (140 MiB / 50.0% CPU) |
Not possible (no QUIC) | Not possible (no QUIC) |
25387 (154 MiB / 46.5% CPU) |
25387 (154 MiB / 46.5% CPU) |
| HTTP/2 · TLS | HTTP/1 · plain | 🥇 34630 (118 MiB / 52.0% CPU) |
🥇 34630 (118 MiB / 52.0% CPU) |
14998 (97 MiB / 19.4% CPU) |
14998 (97 MiB / 19.4% CPU) |
29299 (121 MiB / 49.6% CPU) |
29299 (121 MiB / 49.6% CPU) |
| HTTP/2 · TLS | HTTP/1 · TLS | 🥇 26593 (115 MiB / 50.4% CPU) |
🥇 26593 (115 MiB / 50.4% CPU) |
Not possible | Not possible |
23102 (128 MiB / 49.8% CPU) |
23102 (128 MiB / 49.8% CPU) |
| HTTP/2 · TLS | HTTP/2 · plain | 🥇 57728 (112 MiB / 39.3% CPU) |
🥇 57728 (112 MiB / 39.3% CPU) |
Not possible | Not possible |
39242 (127 MiB / 46.9% CPU) |
39242 (127 MiB / 46.9% CPU) |
| HTTP/2 · TLS | HTTP/2 · TLS | 🥇 48512 (115 MiB / 38.9% CPU) |
🥇 48512 (115 MiB / 38.9% CPU) |
Not possible | Not possible |
33817 (128 MiB / 45.9% CPU) |
33817 (128 MiB / 45.9% CPU) |
| HTTP/2 · TLS | HTTP/3 · QUIC | 🥇 25113 (146 MiB / 49.1% CPU) |
🥇 25113 (146 MiB / 49.1% CPU) |
Not possible (no QUIC) | Not possible (no QUIC) |
22435 (156 MiB / 46.5% CPU) |
22435 (156 MiB / 46.5% CPU) |
| HTTP/3 · QUIC | HTTP/1 · plain | 🥇 20531 (143 MiB / 49.8% CPU) |
🥇 20531 (143 MiB / 49.8% CPU) |
12370 (102 MiB / 22.6% CPU) |
15142 (102 MiB / 22.6% CPU) |
18328 (179 MiB / 50.1% CPU) |
18328 (179 MiB / 50.1% CPU) |
| HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 15566 (147 MiB / 47.3% CPU) |
🥇 15566 (147 MiB / 47.3% CPU) |
Not possible | Not possible |
14627 (194 MiB / 50.6% CPU) |
14627 (194 MiB / 50.6% CPU) |
| HTTP/3 · QUIC | HTTP/2 · plain | 🥇 26473 (139 MiB / 53.1% CPU) |
🥇 26473 (139 MiB / 53.1% CPU) |
Not possible (no H3→H2) | Not possible (no H3→H2) |
23598 (195 MiB / 48.3% CPU) |
23598 (195 MiB / 48.3% CPU) |
| HTTP/3 · QUIC | HTTP/2 · TLS | 🥇 23194 (147 MiB / 51.4% CPU) |
🥇 23194 (147 MiB / 51.4% CPU) |
Not possible (no H3→H2) | Not possible (no H3→H2) |
21588 (190 MiB / 47.7% CPU) |
21588 (190 MiB / 47.7% CPU) |
| HTTP/3 · QUIC | HTTP/3 · QUIC | 🥇 18908 (149 MiB / 46.9% CPU) |
🥇 18908 (149 MiB / 46.9% CPU) |
Not possible (no QUIC) | Not possible (no QUIC) |
15495 (192 MiB / 48.1% CPU) |
15495 (192 MiB / 48.1% CPU) |
Same Client×Origin wires with interception on (compare-product 32960766249). Lite = no-op handlers (unchanged-lite finish). Full = mutating handlers (x-twp-rps-probe) forcing non-lite re-encode. nginx/YARP cannot MITM. Lite÷Reverse / Full÷Reverse vs bare reverse (same job).
Post-fix note (2026-08-26): Phase 2 closed the H3→H1 Full gap: preencoded QPACK relay was gated on empty ViaHeaderPseudonym while the default is "titanium-web-proxy". Cool paired laptop gates (h3h1-spot/, c=64): H3→H1 plain Full÷Reverse 0.91×, H3→H1 TLS 0.995×, H1 plain 0.91× — all ≥0.80. Published table below is still pre-fix CI; remasure via GHA compare-product ×3 is pending.
| Client | Origin | Lite sustain | Full sustain | Lite÷Reverse | Full÷Reverse |
|---|---|---|---|---|---|
| HTTP/1 · plain | HTTP/1 · plain | 🥇 31341 (84 MiB / 50.5% CPU) |
25750 (95 MiB / 50.7% CPU) |
0.97× | 0.79× |
| HTTP/1 · plain | HTTP/1 · TLS | 🥇 24143 (107 MiB / 51.3% CPU) |
19549 (114 MiB / 51.0% CPU) |
1.00× | 0.81× |
| HTTP/1 · plain | HTTP/2 · plain | 🥇 38706 (135 MiB / 53.0% CPU) |
36217 (140 MiB / 52.6% CPU) |
1.00× | 0.94× |
| HTTP/1 · plain | HTTP/2 · TLS | 🥇 30144 (147 MiB / 51.5% CPU) |
29575 (141 MiB / 51.1% CPU) |
0.97× | 0.95× |
| HTTP/1 · plain | HTTP/3 · QUIC | 🥇 21802 (134 MiB / 53.1% CPU) |
22335 (132 MiB / 52.9% CPU) |
0.98× | 1.00× |
| HTTP/1 · TLS | HTTP/1 · plain | 🥇 22828 (110 MiB / 50.5% CPU) |
18135 (111 MiB / 50.8% CPU) |
0.98× | 0.78× |
| HTTP/1 · TLS | HTTP/1 · TLS | 🥇 19298 (109 MiB / 49.2% CPU) |
15947 (120 MiB / 49.6% CPU) |
1.01× | 0.83× |
| HTTP/1 · TLS | HTTP/2 · plain | 🥇 26264 (152 MiB / 50.6% CPU) |
25256 (165 MiB / 51.2% CPU) |
1.00× | 0.96× |
| HTTP/1 · TLS | HTTP/2 · TLS | 🥇 22284 (157 MiB / 49.5% CPU) |
22285 (153 MiB / 49.6% CPU) |
0.94× | 0.94× |
| HTTP/1 · TLS | HTTP/3 · QUIC | 🥇 17158 (154 MiB / 51.9% CPU) |
16953 (149 MiB / 51.7% CPU) |
0.99× | 0.98× |
| HTTP/2 · plain | HTTP/1 · plain | 🥇 35954 (111 MiB / 53.8% CPU) |
35149 (114 MiB / 53.9% CPU) |
0.97× | 0.95× |
| HTTP/2 · plain | HTTP/1 · TLS | 🥇 27724 (122 MiB / 52.6% CPU) |
26872 (123 MiB / 52.4% CPU) |
0.97× | 0.94× |
| HTTP/2 · plain | HTTP/2 · plain | 🥇 57129 (101 MiB / 48.2% CPU) |
14442 (96 MiB / 51.5% CPU) |
0.88× | 0.22× |
| HTTP/2 · plain | HTTP/2 · TLS | 🥇 45119 (99 MiB / 45.6% CPU) |
9618 (98 MiB / 49.4% CPU) |
0.88× | 0.19× |
| HTTP/2 · plain | HTTP/3 · QUIC | 🥇 26558 (136 MiB / 50.2% CPU) |
26568 (140 MiB / 51.2% CPU) |
1.01× | 1.01× |
| HTTP/2 · TLS | HTTP/1 · plain | 🥇 32900 (120 MiB / 53.4% CPU) |
33051 (121 MiB / 53.2% CPU) |
0.95× | 0.95× |
| HTTP/2 · TLS | HTTP/1 · TLS | 🥇 25456 (116 MiB / 51.0% CPU) |
25225 (123 MiB / 50.7% CPU) |
0.96× | 0.95× |
| HTTP/2 · TLS | HTTP/2 · plain | 🥇 50915 (110 MiB / 46.8% CPU) |
13907 (108 MiB / 49.9% CPU) |
0.88× | 0.24× |
| HTTP/2 · TLS | HTTP/2 · TLS | 🥇 42013 (109 MiB / 44.5% CPU) |
9756 (108 MiB / 48.7% CPU) |
0.87× | 0.20× |
| HTTP/2 · TLS | HTTP/3 · QUIC | 🥇 24834 (139 MiB / 49.7% CPU) |
24670 (152 MiB / 50.8% CPU) |
0.99× | 0.98× |
| HTTP/3 · QUIC | HTTP/1 · plain | 🥇 19193 (138 MiB / 50.6% CPU) |
13232 (136 MiB / 48.4% CPU) |
0.93× | 0.64× |
| HTTP/3 · QUIC | HTTP/1 · TLS | 🥇 15251 (158 MiB / 48.7% CPU) |
9627 (153 MiB / 47.0% CPU) |
0.98× | 0.62× |
| HTTP/3 · QUIC | HTTP/2 · plain | 🥇 24530 (152 MiB / 55.6% CPU) |
24462 (158 MiB / 55.7% CPU) |
0.93× | 0.92× |
| HTTP/3 · QUIC | HTTP/2 · TLS | 🥇 21297 (157 MiB / 53.4% CPU) |
21013 (154 MiB / 54.0% CPU) |
0.92× | 0.91× |
| HTTP/3 · QUIC | HTTP/3 · QUIC | 🥇 16680 (147 MiB / 48.6% CPU) |
16729 (154 MiB / 48.0% CPU) |
0.88× | 0.88× |
| HTTP/1 · plain (CONNECT) | HTTP/1 · TLS | 🥇 14398 (118 MiB / 52.1% CPU) |
14522 (135 MiB / 51.5% CPU) |
n/a | n/a |
The tables above use ~64 B keep-alive GET on loopback. On Linux H1 reverse, nginx leads; YARP sits near TWP. Heavier bodies, POSTs, TLS handshake cost, and lossy/HOL workloads (below) change the picture. MITM rows remain TWP-only. nginx HTTP/3 is inbound-terminate only (see note above).
For tiny JSON responses (~64 B) on loopback, that ordering is not expected: topology (TLS hop count, terminate vs MITM) dominates; HTTP/2 and HTTP/3 help multiplexing, not single-origin tiny-GET RPS. See the lossy tables below for a workload where protocol design matters.
Separate from the tiny-GET matrix. Same measurement environments. Modes: compare-bodies, compare-post, compare-lossy, compare-tls-cost, compare-arch in RpsLoadProbe. 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.
Median of 3 repeats on windows-latest @ 9d7c2966. Source: Actions 32871900682 (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 | 🥇 10,297 (99 MiB / 45.5% CPU) |
10,564 (99 MiB / 45.5% CPU) |
717 (137 MiB / 24.6% CPU) |
739 (137 MiB / 24.6% CPU) |
9,283 (110 MiB / 44.7% CPU) |
9,413 (110 MiB / 44.7% CPU) |
| 64 KiB | HTTP/2 · TLS | HTTP/1 · plain | 🥇 9,135 (168 MiB / 46.2% CPU) |
9,135 (168 MiB / 46.2% CPU) |
626 (137 MiB / 24.8% CPU) |
637 (137 MiB / 24.8% CPU) |
7,533 (108 MiB / 49.7% CPU) |
7,745 (108 MiB / 49.7% CPU) |
| 64 KiB | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 4,728 (108 MiB / 39.8% CPU) |
4,900 (108 MiB / 39.8% CPU) |
Not possible (no QUIC) | Not possible (no QUIC) |
3,995 (177 MiB / 49.0% CPU) |
4,035 (177 MiB / 49.0% CPU) |
| 256 KiB | HTTP/1 · TLS | HTTP/1 · plain | 🥇 3,056 (88 MiB / 42.6% CPU) |
3,271 (88 MiB / 42.6% CPU) |
191 (136 MiB / 24.8% CPU) |
194 (136 MiB / 24.8% CPU) |
2,488 (115 MiB / 45.7% CPU) |
2,839 (115 MiB / 45.7% CPU) |
| 256 KiB | HTTP/2 · TLS | HTTP/1 · plain | 🥇 2,568 (115 MiB / 38.9% CPU) |
2,639 (115 MiB / 38.9% CPU) |
163 (137 MiB / 24.9% CPU) |
163 (137 MiB / 24.9% CPU) |
2,003 (132 MiB / 41.6% CPU) |
2,003 (132 MiB / 41.6% CPU) |
| 256 KiB | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 1,210 (88 MiB / 39.3% CPU) |
1,254 (88 MiB / 39.3% CPU) |
Not possible (no QUIC) | Not possible (no QUIC) |
1,100 (125 MiB / 43.9% CPU) |
1,136 (125 MiB / 43.9% 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×.
Median of 3 repeats @ 9d7c2966. Source: Actions 32871900682 (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,902 (170 MiB / 44.4% CPU) |
7,902 (170 MiB / 44.4% CPU) |
5,518 (96 MiB / 52.5% CPU) |
5,518 (96 MiB / 52.5% CPU) |
6,420 (158 MiB / 48.8% CPU) |
6,420 (158 MiB / 48.8% CPU) |
| 64 KiB | HTTP/2 · TLS | HTTP/1 · plain | 🥇 5,831 (218 MiB / 38.9% CPU) |
5,831 (218 MiB / 38.9% CPU) |
1,790 (95 MiB / 23.6% CPU) |
1,925 (95 MiB / 23.6% CPU) |
4,739 (161 MiB / 45.9% CPU) |
4,739 (161 MiB / 45.9% CPU) |
| 64 KiB | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 5,384 (177 MiB / 44.5% CPU) |
5,384 (177 MiB / 44.5% CPU) |
1,614 (113 MiB / 22.0% CPU) |
1,718 (113 MiB / 22.0% CPU) |
4,236 (219 MiB / 51.1% CPU) |
4,236 (219 MiB / 51.1% CPU) |
| 256 KiB | HTTP/1 · TLS | HTTP/1 · plain | 🥇 2,742 (121 MiB / 37.2% CPU) |
2,742 (121 MiB / 37.2% CPU) |
1,732 (95 MiB / 53.3% CPU) |
1,732 (95 MiB / 53.3% CPU) |
2,142 (171 MiB / 45.9% CPU) |
2,142 (171 MiB / 45.9% CPU) |
| 256 KiB | HTTP/2 · TLS | HTTP/1 · plain | 🥇 1,535 (206 MiB / 32.7% CPU) |
1,535 (206 MiB / 32.7% CPU) |
538 (96 MiB / 18.6% CPU) |
538 (96 MiB / 18.6% CPU) |
1,319 (164 MiB / 42.7% CPU) |
1,330 (164 MiB / 42.7% CPU) |
| 256 KiB | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 1,432 (148 MiB / 43.1% CPU) |
1,432 (148 MiB / 43.1% CPU) |
448 (107 MiB / 23.3% CPU) |
448 (107 MiB / 23.3% CPU) |
1,270 (218 MiB / 47.4% CPU) |
1,270 (218 MiB / 47.4% 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.
Median of 3 repeats on windows-latest @ 9d7c2966. Source: Actions 32866714851 (compare-post).
| Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | YARP sustain | YARP peak |
|---|---|---|---|---|---|---|---|
| HTTP/1 · TLS | HTTP/1 · plain | 🥇 8,612 (84 MiB / 42.0% CPU) |
9,133 (84 MiB / 42.0% CPU) |
497 (138 MiB / 24.8% CPU) |
538 (138 MiB / 24.8% CPU) |
5,837 (96 MiB / 56.0% CPU) |
6,283 (96 MiB / 56.0% CPU) |
| HTTP/2 · TLS | HTTP/1 · plain | 🥇 6,241 (196 MiB / 49.7% CPU) |
6,242 (196 MiB / 49.7% CPU) |
486 (139 MiB / 24.9% CPU) |
493 (139 MiB / 24.9% CPU) |
5,106 (118 MiB / 50.9% CPU) |
5,147 (118 MiB / 50.9% CPU) |
| HTTP/3 · QUIC | HTTP/1 · plain | 🥇 2,691 (118 MiB / 43.7% CPU) |
2,937 (118 MiB / 43.7% CPU) |
Not possible (no QUIC) | Not possible (no QUIC) |
2,549 (132 MiB / 49.5% CPU) |
2,747 (132 MiB / 49.5% CPU) |
TWP leads H1 POST (1.5× YARP), H2 POST (1.2× YARP), and H3 POST (~1.1× YARP).
Median of 3 repeats @ 9d7c2966. Source: Actions 32866714851 (compare-post).
| Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | YARP sustain | YARP peak |
|---|---|---|---|---|---|---|---|
| HTTP/1 · TLS | HTTP/1 · plain | 🥇 6,746 (127 MiB / 41.8% CPU) |
6,746 (127 MiB / 41.8% CPU) |
5,443 (98 MiB / 44.8% CPU) |
5,443 (98 MiB / 44.8% CPU) |
4,410 (176 MiB / 53.8% CPU) |
4,410 (176 MiB / 53.8% CPU) |
| HTTP/2 · TLS | HTTP/1 · plain | 🥇 4,720 (220 MiB / 47.0% CPU) |
4,720 (220 MiB / 47.0% CPU) |
2,362 (106 MiB / 21.8% CPU) |
2,475 (106 MiB / 21.8% CPU) |
3,616 (144 MiB / 48.3% CPU) |
3,626 (144 MiB / 48.3% CPU) |
| HTTP/3 · QUIC | HTTP/1 · plain | 🥇 3,306 (223 MiB / 43.7% CPU) |
3,306 (223 MiB / 43.7% CPU) |
835 (111 MiB / 24.6% CPU) |
835 (111 MiB / 24.6% CPU) |
2,959 (260 MiB / 51.5% CPU) |
2,959 (260 MiB / 51.5% 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×.
Userspace 5 ms one-way delay + 1% TCP connection stall (H1/H2) or UDP datagram drop (H3); 64 KiB GET. H1/H2: median of 3 repeats on windows-latest @ 9d7c2966 — 32866717729 (compare-lossy). H3: GHA Windows userspace UDP shim collapses (sustain 0); published H3 row is the laptop quic-http3 remasure under the same delay/loss workload (Performance Local Lab, `windows-20260822-lossy-h3-quic/).
| Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | YARP sustain | YARP peak |
|---|---|---|---|---|---|---|---|
| HTTP/1 · TLS | HTTP/1 · plain | 🥇 663 (98 MiB / 4.8% CPU) |
663 (98 MiB / 4.8% CPU) |
634 (137 MiB / 19.6% CPU) |
634 (137 MiB / 19.6% CPU) |
662 (113 MiB / 5.7% CPU) |
662 (113 MiB / 5.7% CPU) |
| HTTP/2 · TLS | HTTP/1 · plain | 🥇 59 (122 MiB / 1.7% CPU) |
88 (122 MiB / 1.7% CPU) |
18 (137 MiB / 0.5% CPU) |
18 (137 MiB / 0.5% CPU) |
18 (84 MiB / 1.1% CPU) |
18 (84 MiB / 1.1% CPU) |
| HTTP/3 · QUIC | HTTP/1 · plain | 🥇 1,572 | 1,572 | Not possible (no QUIC) | Not possible | 0 | 50 |
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 remasure kept above).
Median of 3 repeats @ 9d7c2966. Source: 32866717729 (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,201 (142 MiB / 12.9% CPU) |
1,201 (142 MiB / 12.9% CPU) |
🥇 1,206 (97 MiB / 11.8% CPU) |
1,206 (97 MiB / 11.8% CPU) |
1,197 (144 MiB / 16.6% CPU) |
1,197 (144 MiB / 16.6% CPU) |
| HTTP/2 · TLS | HTTP/1 · plain | 🥇 309 (191 MiB / 6.5% CPU) |
309 (191 MiB / 6.5% CPU) |
40 (95 MiB / 0.3% CPU) |
42 (95 MiB / 0.3% CPU) |
40 (118 MiB / 1.4% CPU) |
44 (118 MiB / 1.4% CPU) |
| HTTP/3 · QUIC | HTTP/1 · plain | 🥇 332 (149 MiB / 13.3% CPU) |
332 (149 MiB / 13.3% CPU) |
88 (105 MiB / 2.7% CPU) |
88 (105 MiB / 2.7% CPU) |
330 (176 MiB / 17.9% CPU) |
330 (176 MiB / 17.9% CPU) |
TWP H2 HOL ≫ YARP (~7.7×). H3 TWP÷YARP ≈ 1×.
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 @ 9d7c2966 (32866720742) (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.
| 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 (91 MiB / 3.8% CPU) |
248 (91 MiB / 3.8% CPU) |
209 (140 MiB / 24.7% CPU) |
209 (140 MiB / 24.7% CPU) |
248 (110 MiB / 4.9% CPU) |
248 (110 MiB / 4.9% CPU) |
| Slow consumer (256 KiB GET, throttled client read) | HTTP/2 · TLS | HTTP/1 · plain |
248 (125 MiB / 3.8% CPU) |
248 (125 MiB / 3.8% CPU) |
166 (137 MiB / 24.6% CPU) |
166 (137 MiB / 24.6% CPU) |
🥇 249 (112 MiB / 7.4% CPU) |
249 (112 MiB / 7.4% CPU) |
| Slow consumer (256 KiB GET, throttled client read) | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 264 (99 MiB / 17.1% CPU) |
264 (99 MiB / 17.1% CPU) |
Not possible (no QUIC) | Not possible (no QUIC) |
264 (160 MiB / 21.3% CPU) |
264 (160 MiB / 21.3% CPU) |
| Early response (origin writes after first request chunk) | HTTP/1 · TLS | HTTP/1 · plain | 🥇 6,507 (84 MiB / 43.0% CPU) |
6,881 (84 MiB / 43.0% CPU) |
347 (137 MiB / 24.9% CPU) |
419 (137 MiB / 24.9% CPU) |
3,256 (117 MiB / 39.5% CPU) |
3,606 (117 MiB / 39.5% CPU) |
| Early response (origin writes after first request chunk) | HTTP/2 · TLS | HTTP/1 · plain | 🥇 5,011 (153 MiB / 48.6% CPU) |
5,011 (153 MiB / 48.6% CPU) |
0 (139 MiB / 24.8% CPU) |
383 (139 MiB / 24.8% CPU) |
2,967 (97 MiB / 39.6% CPU) |
3,184 (97 MiB / 39.6% CPU) |
| Early response (origin writes after first request chunk) | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 2,153 (125 MiB / 42.8% CPU) |
2,256 (125 MiB / 42.8% CPU) |
Not possible (no QUIC) | Not possible (no QUIC) |
1,874 (154 MiB / 53.3% CPU) |
2,034 (154 MiB / 53.3% CPU) |
| Duplex (both directions live) | HTTP/2 · TLS | HTTP/2 · TLS | 🥇 784 (124 MiB / 13.2% CPU) |
1,270 (124 MiB / 13.2% CPU) |
Not possible | Not possible |
18 (120 MiB / 30.7% CPU) |
2,135 (120 MiB / 30.7% CPU) |
| Duplex (WebSocket / extended CONNECT) | HTTP/1 · TLS | HTTP/1 · plain | 🥇 32,785 (97 MiB / 40.8% CPU) |
32,785 (97 MiB / 40.8% CPU) |
19,252 (138 MiB / 24.9% CPU) |
19,803 (138 MiB / 24.9% CPU) |
30,820 (85 MiB / 43.6% CPU) |
30,820 (85 MiB / 43.6% CPU) |
| 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 | 🥇 468 (115 MiB / 9.5% CPU) |
468 (115 MiB / 9.5% CPU) |
416 (96 MiB / 9.6% CPU) |
416 (96 MiB / 9.6% CPU) |
418 (138 MiB / 13.6% CPU) |
418 (138 MiB / 13.6% CPU) |
| Slow consumer (256 KiB GET, throttled client read) | HTTP/2 · TLS | HTTP/1 · plain |
473 (131 MiB / 17.8% CPU) |
473 (131 MiB / 17.8% CPU) |
462 (96 MiB / 17.3% CPU) |
462 (96 MiB / 17.3% CPU) |
🥇 474 (146 MiB / 23.5% CPU) |
474 (146 MiB / 23.5% CPU) |
| Slow consumer (256 KiB GET, throttled client read) | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 474 (125 MiB / 34.3% CPU) |
474 (125 MiB / 34.3% CPU) |
119 (100 MiB / 11.4% CPU) |
238 (100 MiB / 11.4% CPU) |
472 (193 MiB / 40.0% CPU) |
472 (193 MiB / 40.0% CPU) |
| Early response (origin writes after first request chunk) | HTTP/1 · TLS | HTTP/1 · plain | 🥇 5,039 (149 MiB / 45.3% CPU) |
5,039 (149 MiB / 45.3% CPU) |
3,904 (98 MiB / 49.4% CPU) |
3,904 (98 MiB / 49.4% CPU) |
3,425 (175 MiB / 55.3% CPU) |
3,425 (175 MiB / 55.3% CPU) |
| Early response (origin writes after first request chunk) | HTTP/2 · TLS | HTTP/1 · plain | 🥇 3,661 (233 MiB / 44.1% CPU) |
3,661 (233 MiB / 44.1% CPU) |
0 (103 MiB / 24.6% CPU) |
1,803 (103 MiB / 24.6% CPU) |
2,370 (148 MiB / 47.6% CPU) |
2,570 (148 MiB / 47.6% CPU) |
| Early response (origin writes after first request chunk) | HTTP/3 · QUIC | HTTP/1 · plain | 🥇 3,018 (193 MiB / 43.5% CPU) |
3,018 (193 MiB / 43.5% CPU) |
0 (114 MiB / 24.7% CPU) |
545 (114 MiB / 24.7% CPU) |
2,232 (262 MiB / 46.7% CPU) |
2,232 (262 MiB / 46.7% CPU) |
| Duplex (both directions live) | HTTP/2 · TLS | HTTP/2 · TLS | 🥇 185 (124 MiB / 9.0% CPU) |
282 (124 MiB / 9.0% CPU) |
Not possible | Not possible |
138 (139 MiB / 44.6% CPU) |
1,882 (139 MiB / 44.6% CPU) |
| Duplex (WebSocket / extended CONNECT) | HTTP/1 · TLS | HTTP/1 · plain |
35,088 (121 MiB / 44.5% CPU) |
35,088 (121 MiB / 44.5% CPU) |
🥇 39,303 (97 MiB / 37.1% CPU) |
39,303 (97 MiB / 37.1% CPU) |
32,013 (122 MiB / 44.9% CPU) |
32,013 (122 MiB / 44.9% 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.
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.
Median of 3 repeats on windows-latest @ 9d7c2966. Source: Actions 32866723562 (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,709 (82 MiB / 48.3% CPU) |
20,709 (82 MiB / 48.3% CPU) |
9,096 (137 MiB / 24.8% CPU) |
9,158 (137 MiB / 24.8% CPU) |
17,934 (103 MiB / 48.1% CPU) |
17,934 (103 MiB / 48.1% CPU) |
| New-connection · tiny GET | 🥇 739 (65 MiB / 10.4% CPU) |
745 (65 MiB / 10.4% CPU) |
252 (136 MiB / 24.2% CPU) |
256 (136 MiB / 24.2% CPU) |
591 (93 MiB / 10.1% CPU) |
607 (93 MiB / 10.1% CPU) |
| Keep-alive · 256 KiB GET | 🥇 2,698 (87 MiB / 46.2% CPU) |
2,971 (87 MiB / 46.2% CPU) |
172 (137 MiB / 24.7% CPU) |
174 (137 MiB / 24.7% CPU) |
2,569 (134 MiB / 44.0% CPU) |
2,569 (134 MiB / 44.0% CPU) |
Median of 3 repeats @ 9d7c2966. Source: Actions 32866723562 (compare-tls-cost).
| Workload | TWP sustain | TWP peak | nginx sustain | nginx peak | YARP sustain | YARP peak |
|---|---|---|---|---|---|---|
| Keep-alive · tiny GET |
23,687 (104 MiB / 49.1% CPU) |
23,687 (104 MiB / 49.1% CPU) |
🥇 25,897 (95 MiB / 41.3% CPU) |
26,177 (95 MiB / 41.3% CPU) |
20,716 (133 MiB / 50.5% CPU) |
20,716 (133 MiB / 50.5% CPU) |
| New-connection · tiny GET |
962 (124 MiB / 47.5% CPU) |
973 (124 MiB / 47.5% CPU) |
🥇 1,005 (96 MiB / 44.2% CPU) |
1,008 (96 MiB / 44.2% CPU) |
941 (148 MiB / 45.8% CPU) |
941 (148 MiB / 45.8% CPU) |
| Keep-alive · 256 KiB GET | 🥇 2,690 (126 MiB / 36.5% CPU) |
2,690 (126 MiB / 36.5% CPU) |
1,749 (96 MiB / 53.6% CPU) |
1,749 (96 MiB / 53.6% CPU) |
2,146 (172 MiB / 45.9% CPU) |
2,146 (172 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.
| 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 |
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;
};