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). Absolute RPS varies by hardware, OS, and background load — 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 methodology behind these numbers — the harness, noise control, and the profiling techniques used to find each hotspot — see Performance Profiling.
| OS | Windows 11 (10.0.26200) |
| CPU | 11th Gen Intel Core i7-1185G7 @ 3.00 GHz (8 logical processors) |
| RAM | 31.8 GiB |
| Runtime | .NET 10.0.10 |
| nginx | nginx/Windows 1.31.3 |
| YARP | Yarp.ReverseProxy 2.3.0 |
| Harness | RpsLoadProbe Release; arms run sequentially |
| OS | Ubuntu 24.04.4 LTS |
| CPU | AMD EPYC (4 logical processors on the VM; runners this pass were 7763 / 9V74) |
| RAM | 15.6 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 |
How to read the tables
- Mode: Reverse = transparent fixed-forward (may TLS-terminate to a cleartext origin, or re-encrypt to a configured HTTPS/QUIC origin). MITM = proxy decrypts the client crypto and speaks TLS/QUIC to the origin (or explicit decrypt proxy), so both legs are visible in the clear inside TWP. nginx and YARP cannot do MITM.
- Sustainable = last concurrency that still met error/latency SLOs. Peak = highest RPS in that ramp.
- Winner = highest sustainable RPS among products that have a number on that row. Left blank when only TWP can run the path (no fair multi-product comparison).
- Not possible = product cannot do that path. Not measured = path exists but no published number yet for that OS.
pwsh tools/RpsLoadProbe/run-rps.ps1 -Mode compare-same
pwsh tools/RpsLoadProbe/run-rps.ps1 -Mode compare-terminate
pwsh tools/RpsLoadProbe/run-rps.ps1 -Mode compare-bridges
pwsh tools/RpsLoadProbe/run-rps.ps1 -Mode compare-mitm
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-costClient / origin: HTTP version and whether TLS is used (plain = cleartext, TLS = encrypted, QUIC = HTTP/3).
| Mode | Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | YARP sustain | YARP peak | Winner |
|---|---|---|---|---|---|---|---|---|---|
| Reverse | HTTP/1 · plain | HTTP/1 · plain | 33,400 | 33,400 | 15,196 | 18,806 | 37,440 | 37,440 | YARP |
| Reverse | HTTP/1 · TLS | HTTP/1 · plain | 30,736 | 30,736 | 10,252 | 13,741 | 33,339 | 33,339 | YARP |
| Reverse | HTTP/1 · TLS | HTTP/2 · TLS | 36,086 | 36,086 | Not possible | Not possible | 39,768 | 39,768 | YARP |
| Reverse | HTTP/1 · TLS | HTTP/3 · QUIC | 25,125 | 25,125 | Not possible (no QUIC) | Not possible | 27,596 | 27,596 | YARP |
| Reverse | HTTP/2 · plain | HTTP/1 · plain | 52,183 | 52,183 | Not possible | Not possible | 52,543 | 52,543 | YARP |
| Reverse | HTTP/2 · plain | HTTP/2 · plain | 100,568 | 100,568 | Not possible | Not possible | 86,021 | 86,021 | TWP |
| Reverse | HTTP/2 · plain | HTTP/2 · TLS | 88,006 | 88,006 | Not possible | Not possible | 84,634 | 84,634 | TWP |
| Reverse | HTTP/2 · plain | HTTP/3 · QUIC | 40,075 | 40,075 | Not possible (no QUIC) | Not possible | 42,235 | 42,235 | YARP |
| Reverse | HTTP/2 · TLS | HTTP/1 · plain | 49,548 | 49,548 | 15,793 | 15,793 | 49,072 | 49,072 | TWP |
| Reverse | HTTP/2 · TLS | HTTP/2 · plain | 94,238 | 94,238 | Not possible | Not possible | 81,266 | 81,266 | TWP |
| Reverse | HTTP/2 · TLS | HTTP/3 · QUIC | 34,506 | 34,506 | Not possible (no QUIC) | Not possible | 35,388 | 35,388 | YARP |
| Reverse | HTTP/3 · QUIC | HTTP/1 · plain | 19,517 | 20,320 | Not possible (no QUIC) | Not possible | 28,145 | 28,145 | YARP |
| Reverse | HTTP/3 · QUIC | HTTP/2 · TLS | 32,883 | 32,883 | Not possible (no QUIC) | Not possible | 38,892 | 38,892 | YARP |
| Reverse | HTTP/3 · QUIC | HTTP/3 · QUIC | 24,097 | 24,097 | Not possible (no QUIC) | Not possible | 25,463 | 25,463 | YARP |
| MITM | HTTP/1 · TLS | HTTP/1 · TLS | 33,691 | 33,691 | Not possible (no MITM) | Not possible | Not possible (no MITM) | Not possible | |
| MITM | HTTP/2 · TLS | HTTP/1 · TLS | 48,925 | 48,925 | Not possible (no MITM) | Not possible | Not possible (no MITM) | Not possible | |
| MITM | HTTP/2 · TLS | HTTP/2 · TLS | 99,545 | 99,545 | Not possible (no MITM) | Not possible | Not possible (no MITM) | Not possible | |
| MITM | HTTP/3 · QUIC | HTTP/1 · TLS | 21,811 | 21,811 | Not possible (no QUIC) | Not possible | Not possible (no MITM) | Not possible | |
| MITM | HTTP/3 · QUIC | HTTP/2 · TLS | 36,473 | 36,473 | Not possible (no QUIC) | Not possible | Not possible (no MITM) | Not possible | |
| MITM | HTTP/3 · QUIC | HTTP/3 · QUIC | 38,700 | 38,700 | Not possible (no QUIC) | Not possible | Not possible (no MITM) | Not possible |
Windows reverse tiny-GET: base matrix 2026-08-20 High-perf, Linux-matched harness (warmup 2s / measure 8s; concurrency 8, 16, 32, 64; median of 3 repeats except H2 TLS→H3 and H3→H1/H2, which have 2). CSVs under tools/RpsLoadProbe/results/windows-20260820/ (compare-same, compare-bridges). MITM and heavier reverse: 1-repeat follow-up under windows-20260820-quick/. Absolute RPS swings with sequential-arm heat; prefer TWP÷YARP ratios.
2026-08-21 remasure (HEAD 6f94df49): H1 plain, H1 TLS terminate, and H1→H2 refreshed with paired cool/soft A/B after keep-alive parse/framing cuts, H1→H2 IsFastPath deliver, coalesce tiny headers+body, ASCII-lowercase H2 names, and small fixed-length H2 body materialize. H1→H2 = mean of both arm orders at c=32 (win-h1h2-smallbody, win-h1h2-smallbody-yarpfirst); H1 plain = YARP-first (win-h1plain-yarpfirst); H1 TLS = TWP-then-YARP (win-matrix-post-smallbody). Other Windows rows still 2026-08-20.
Load generators: Reverse inbound H3 arms use dotnet-httpclient (http_version=3.0, RequestVersionExact) after dual-listen reverse H3. MITM H3→H2 / H3→H3 reuse dual-listen transparent reverse (reverse-http3-to-http2, reverse-http3). Older UDP-only quic-http3 MITM H3→H1 numbers are not comparable to HttpClient reverse twins.
Matched HttpClient TWP÷YARP (this pass): H1 plain ≈ 0.89 (33,400 / 37,440; was 0.82). H1 TLS terminate ≈ 0.92 (30,736 / 33,339; was 0.86). H1→H2 ≈ 0.91 (36,086 / 39,768; was 0.70 — both orders 0.90 / 0.92). H1→H3 ≈ 0.91 (25,125 / 27,596). h2c→h2c ≈ 1.17× (100,568 / 86,021; TWP leads). h2c→H2 TLS ≈ 1.04× (88,006 / 84,634; TWP leads). H2 TLS→h2c ≈ 1.16× (94,238 / 81,266; TWP leads). H2 TLS→H1 ≈ 1.01× (49,548 / 49,072; TWP leads). h2c→H1 ≈ 0.99. h2c→H3 ≈ 0.95. H2 TLS→H3 ≈ 0.98. H3→H3 ≈ 0.95 (24,097 / 25,463). H3→H2 ≈ 0.85 (32,883 / 38,892). H3→H1 ≈ 0.69 (19,517 / 28,145).
MITM÷TWP reverse pass-through twin (1-rep quick): Transparent H1 dual-TLS (reverse-http1-mitm) ÷ H1 TLS terminate ≈ 1.21 (33,691 / 27,762). H2→H1 MITM ÷ H2→H1 cleartext ≈ 0.99 (48,925 / 49,548). H3→H1 MITM ÷ H3→H1 cleartext ≈ 1.12 (21,811 / 19,517). Explicit CONNECT https-mitm ≈ 31,294 sustain / 35,794 peak (CONNECT tax; not the fair twin).
Why H3 absolute RPS ≪ H2 on this box: tiny-GET loopback is not where H3 wins (see below). YARP H3→H3 also tops out ~25k while TWP H2 same-protocol reaches ~90–100k — MsQuic + dual QUIC hops dominate, not TWP architecture. On this sequential Windows pass TWP H3→H3 is 0.95× YARP (Linux still has TWP leading that row).
nginx/Windows is a limited port — use it for same-OS comparison only, not as the industry nginx baseline.
H2 TLS → H1 plain on Windows: fair terminate — TWP leads sustain (~1.01× YARP) in the current table. Absolute RPS swings with background load; treat as same-OS only.
Median of 3 repeats from Actions runs 32335537070 (compare-same), 32335538634 (compare-terminate), 32334393173 (compare-bridges), 32334395907 (compare-mitm). nginx HTTP/3 tiny-GET from 32337905168. Warmup 2s / measure 8s; concurrency 8, 16, 32, 64. Linux nginx is the authoritative nginx baseline. The RPS workflow installs nginx.org mainline (http_v3_module) and libmsquic (QuicListener.IsSupported=true on ubuntu-latest).
TWP÷nginx H1 plain reverse ≈ 0.67; TWP÷YARP H1 plain ≈ 0.94. Prefer ratios over absolute RPS on GHA VMs.
| Mode | Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | YARP sustain | YARP peak | Winner |
|---|---|---|---|---|---|---|---|---|---|
| Reverse | HTTP/1 · plain | HTTP/1 · plain | 26,808 | 26,808 | 39,886 | 39,886 | 28,662 | 28,662 | nginx |
| Reverse | HTTP/1 · TLS | HTTP/1 · plain | 22,592 | 22,592 | 32,234 | 32,234 | 25,863 | 25,863 | nginx |
| Reverse | HTTP/1 · TLS | HTTP/2 · TLS | 18,401 | 18,401 | Not possible | Not possible | 23,194 | 23,194 | YARP |
| Reverse | HTTP/1 · TLS | HTTP/3 · QUIC | 17,030 | 17,030 | Not possible (no H3 origin) | Not possible | 17,630 | 17,630 | YARP |
| Reverse | HTTP/2 · plain | HTTP/1 · plain | 40,459 | 40,459 | Not possible | Not possible | 40,441 | 40,441 | TWP |
| Reverse | HTTP/2 · plain | HTTP/2 · plain | 66,345 | 66,345 | Not possible | Not possible | 49,812 | 49,812 | TWP |
| Reverse | HTTP/2 · plain | HTTP/2 · TLS | 54,696 | 54,696 | Not possible | Not possible | 41,854 | 41,854 | TWP |
| Reverse | HTTP/2 · plain | HTTP/3 · QUIC | 29,621 | 29,621 | Not possible (no H3 origin) | Not possible | 27,928 | 27,928 | TWP |
| Reverse | HTTP/2 · TLS | HTTP/1 · plain | 38,702 | 38,702 | 16,257 | 22,708 | 34,189 | 34,189 | TWP |
| Reverse | HTTP/2 · TLS | HTTP/2 · plain | 59,721 | 59,721 | Not possible | Not possible | 40,008 | 40,008 | TWP |
| Reverse | HTTP/2 · TLS | HTTP/3 · QUIC | 28,063 | 28,063 | Not possible (no H3 origin) | Not possible | 23,698 | 23,698 | TWP |
| Reverse | HTTP/3 · QUIC | HTTP/1 · plain | 22,567 | 22,567 | 0 | 18,996 | 22,002 | 22,002 | TWP |
| Reverse | HTTP/3 · QUIC | HTTP/2 · TLS | 24,708 | 24,708 | Not possible (no H3→H2) | Not possible | 21,754 | 21,754 | TWP |
| Reverse | HTTP/3 · QUIC | HTTP/3 · QUIC | 20,587 | 20,587 | Not possible (no H3 origin) | Not possible | 16,736 | 16,736 | TWP |
| MITM | HTTP/1 · TLS | HTTP/1 · TLS | 30,703 | 30,703 | Not possible (no MITM) | Not possible | Not possible (no MITM) | Not possible | |
| MITM | HTTP/2 · TLS | HTTP/1 · TLS | 38,764 | 38,764 | Not possible (no MITM) | Not possible | Not possible (no MITM) | Not possible | |
| MITM | HTTP/2 · TLS | HTTP/2 · TLS | 52,372 | 52,372 | Not possible (no MITM) | Not possible | Not possible (no MITM) | Not possible | |
| MITM | HTTP/3 · QUIC | HTTP/1 · TLS | 24,036 | 24,036 | Not possible (no MITM) | Not possible | Not possible (no MITM) | Not possible | |
| MITM | HTTP/3 · QUIC | HTTP/2 · TLS | 26,746 | 26,746 | Not possible (no MITM) | Not possible | Not possible (no MITM) | Not possible | |
| MITM | HTTP/3 · QUIC | HTTP/3 · QUIC | 22,346 | 22,346 | Not possible (no MITM) | Not possible | Not possible (no MITM) | Not possible |
On this GHA shape, TWP H1 plain ÷ nginx H1 plain ≈ 0.67 (26,808 / 39,886). H1 TLS terminate ≈ 0.70 (22,592 / 32,234). TWP÷YARP H1 plain ≈ 0.94. Absolute RPS swings by VM; prefer the ratio and median across repeats.
nginx HTTP/3: inbound QUIC terminate → cleartext H1 is now in the harness (nginx-reverse-http3-cleartext, nginx.org 1.31.4). HttpClient/MsQuic negotiates 3.0 and peaks at ~19k RPS, but the error rate stays above the 0.1% SLO (sustain 0). nginx still cannot speak HTTP/3 to an origin (no H3 upstream in this conf), so H3-origin rows stay blank.
YARP HTTP/3: compare-same now includes H3→H3; compare-bridges fills H3→H2. TWP leads both (20,587 / 16,736 and 24,708 / 21,754).
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 in RpsLoadProbe. PUT with the same body is the same proxy work as POST; DELETE with no body matches GET — only POST is published.
Lossy link = userspace shim (not kernel netem): TCP gets per-buffer delay + occasional whole-connection stalls (honest HOL for multiplexed H2); UDP datagram drop exists in the harness but H3 lossy is not published — rechecked at concurrency 8 after H2/H3 streaming work (rps-ramp-20260817-212421): TWP H3 through the UDP shim stayed at 0 sustain (multi-second p99); YARP H3 via the TCP shim also failed to establish. Treat as a measurement limitation (MsQuic + lossy shim), not a capability claim.
1-repeat; warmup 2s / measure 8s; concurrency 8–64. Source: windows-20260820-quick/compare-bodies.
| Body | Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | YARP sustain | YARP peak | Winner |
|---|---|---|---|---|---|---|---|---|---|
| 64 KiB | HTTP/1 · TLS | HTTP/1 · plain | 11,414 | 12,184 | 1,119 | 1,184 | 12,664 | 13,403 | YARP |
| 64 KiB | HTTP/2 · TLS | HTTP/1 · plain | 5,778 | 5,792 | 1,030 | 1,063 | 8,081 | 8,323 | YARP |
| 64 KiB | HTTP/3 · QUIC | HTTP/1 · plain | 1,109 | 1,109 | Not possible (no QUIC) | Not possible | 3,108 | 3,108 | YARP |
| 256 KiB | HTTP/1 · TLS | HTTP/1 · plain | 3,260 | 3,260 | 292 | 301 | 3,882 | 3,934 | YARP |
| 256 KiB | HTTP/2 · TLS | HTTP/1 · plain | 1,294 | 1,353 | 236 | 239 | 2,253 | 2,347 | YARP |
| 256 KiB | HTTP/3 · QUIC | HTTP/1 · plain | 771 | 771 | Not possible (no QUIC) | Not possible | 939 | 939 | YARP |
nginx/Windows collapses on large reverse bodies in this harness; treat as same-OS only.
Median of 3 repeats. Source: Actions 32335540288 (compare-bodies). Warmup 2s / measure 8s.
| Body | Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | YARP sustain | YARP peak | Winner |
|---|---|---|---|---|---|---|---|---|---|
| 64 KiB | HTTP/1 · TLS | HTTP/1 · plain | 6,213 | 6,241 | 7,874 | 7,904 | 6,339 | 6,383 | nginx |
| 64 KiB | HTTP/2 · TLS | HTTP/1 · plain | 4,612 | 4,612 | 3,417 | 3,433 | 4,735 | 4,735 | YARP |
| 64 KiB | HTTP/3 · QUIC | HTTP/1 · plain | 3,181 | 3,181 | Not measured | Not measured | 4,201 | 4,201 | YARP |
| 256 KiB | HTTP/1 · TLS | HTTP/1 · plain | 2,068 | 2,068 | 2,607 | 2,607 | 2,114 | 2,114 | nginx |
| 256 KiB | HTTP/2 · TLS | HTTP/1 · plain | 1,354 | 1,354 | 910 | 935 | 1,338 | 1,338 | TWP |
| 256 KiB | HTTP/3 · QUIC | HTTP/1 · plain | 1,184 | 1,184 | Not measured | Not measured | 1,238 | 1,238 | YARP |
On Linux H1 TLS, TWP÷nginx ≈ 0.79 at 64 KiB and ≈ 0.79 at 256 KiB — better than the tiny-GET ≈0.67 ratio, but nginx still leads sustain when both stay healthy. nginx H2 at 256 KiB stayed healthy this pass (~910 sustain). nginx HTTP/3 body arms ran before the dual-stack QUIC listen fix and never negotiated h3 — left unmeasured rather than published as a fair zero.
1-repeat; warmup 2s / measure 8s. Source: windows-20260820-quick/compare-post.
| Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | YARP sustain | YARP peak | Winner |
|---|---|---|---|---|---|---|---|---|
| HTTP/1 · TLS | HTTP/1 · plain | 6,003 | 6,160 | 383 | 413 | 5,264 | 5,420 | TWP |
| HTTP/2 · TLS | HTTP/1 · plain | 3,519 | 3,519 | 357 | 389 | 4,741 | 4,871 | YARP |
| HTTP/3 · QUIC | HTTP/1 · plain | 0 | 185 | Not possible | Not possible | 1,890 | 1,890 | YARP |
TWP wins H1 POST (~1.14× YARP) and sits at ~0.74 of YARP on H2 POST. H3 POST did not hold the SLO (sustain 0).
Median of 3 repeats. Source: Actions 32335541836 (compare-post).
| Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | YARP sustain | YARP peak | Winner |
|---|---|---|---|---|---|---|---|---|
| HTTP/1 · TLS | HTTP/1 · plain | 3,885 | 3,885 | 4,041 | 4,041 | 3,112 | 3,112 | nginx |
| HTTP/2 · TLS | HTTP/1 · plain | 2,763 | 2,763 | 1,929 | 1,990 | 2,468 | 2,525 | TWP |
| HTTP/3 · QUIC | HTTP/1 · plain | 0 | 2,033 | Not measured | Not measured | 2,592 | 2,592 | YARP |
Linux nginx H1/H2 POST completed this pass (nginx.org 1.31.4; the previous Ubuntu 1.24 arm returned 100% errors). TWP H3 POST peaked at 2,033 but did not hold the error/latency SLO (sustain 0). nginx HTTP/3 POST used the pre-fix IPv4-only QUIC listen and is left unmeasured.
Userspace 5 ms one-way delay + 1% connection stall; 64 KiB GET. 1-repeat. Source: windows-20260820-quick/compare-lossy.
| Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | YARP sustain | YARP peak | Winner |
|---|---|---|---|---|---|---|---|---|
| HTTP/1 · TLS | HTTP/1 · plain | 287 | 536 | 674 | 674 | 678 | 678 | YARP |
| HTTP/2 · TLS | HTTP/1 · plain | 14 | 14 | 14 | 15 | 15 | 15 | YARP |
H1 stays usable; H2 collapses under connection stalls (HOL). Absolute RPS is low because the shim delays every buffer — the point is the protocol shape, not competing with the tiny-GET table.
Median of 3 repeats. Source: Actions 32335543372 (compare-lossy).
| Client | Origin | TWP sustain | TWP peak | nginx sustain | nginx peak | YARP sustain | YARP peak | Winner |
|---|---|---|---|---|---|---|---|---|
| HTTP/1 · TLS | HTTP/1 · plain | 1,168 | 1,168 | 1,220 | 1,220 | 1,217 | 1,217 | nginx |
| HTTP/2 · TLS | HTTP/1 · plain | 40 | 44 | 40 | 44 | 40 | 40 | nginx |
Same story as Windows: H1 stays usable; H2 falls to tens of RPS for all three products. Tiny-GET H1 leadership does not carry over.
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.
1-repeat. Source: windows-20260820-quick/compare-tls-cost.
| Workload | TWP sustain | TWP peak | nginx sustain | nginx peak | YARP sustain | YARP peak | Winner |
|---|---|---|---|---|---|---|---|
| Keep-alive · tiny GET | 35,724 | 39,386 | 17,148 | 18,318 | 39,905 | 39,905 | YARP |
| New-connection · tiny GET | 926 | 926 | 789 | 800 | 1,088 | 1,088 | YARP |
| Keep-alive · 256 KiB GET | 3,349 | 3,349 | 291 | 299 | 3,916 | 3,916 | YARP |
Median of 3 repeats. Source: Actions 32335545066 (compare-tls-cost).
| Workload | TWP sustain | TWP peak | nginx sustain | nginx peak | YARP sustain | YARP peak | Winner |
|---|---|---|---|---|---|---|---|
| Keep-alive · tiny GET | 18,880 | 18,880 | 28,245 | 28,245 | 20,746 | 20,746 | nginx |
| New-connection · tiny GET | 1,106 | 1,113 | 1,005 | 1,009 | 957 | 957 | TWP |
| Keep-alive · 256 KiB GET | 2,126 | 2,126 | 2,627 | 2,627 | 2,134 | 2,134 | nginx |
Verdict (Linux, authoritative nginx): On keep-alive tiny terminate, TWP is ~0.67× nginx and ~0.91× YARP. On new-connection terminate, TWP is ahead of both (~1.10× nginx, ~1.16× YARP). With 256 KiB bodies, nginx still leads sustain when all three are healthy.
| 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) | ~186 µs, ~17.5 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*'| Benchmark | Setup | Mean | Allocated / op |
|---|---|---|---|
| HTTP/1 GET through proxy | Passthrough | 186 µs | 17.5 KB |
| HTTP/2 multiplexed GETs | 10 concurrent streams | 3.0 ms / batch | ~14 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;
};