# Benchmarks quicgate ships a reproducible benchmark suite so you can see the engine's capacity and run it yourself. Numbers vary enormously by CPU, so treat these as a ballpark and reproduce on your own hardware. ## Run it ```bash go test -run='^$' -bench=. -benchmem -benchtime=3s ./internal/engine 2>/dev/null ``` (`2>/dev/null` just hides the engine's start-up logs so the benchmark table is clean.) ## What is measured Each benchmark builds a real engine with one proxy host in front of a trivial local backend, then drives requests through the **full pipeline** (routing → access list / middleware → reverse proxy). The client, quicgate and the backend all run on the same machine over loopback with no TLS, so these are **full round-trip, single-box** figures: a real deployment where the client and the backend live on other machines leaves quicgate more CPU and goes higher. | Benchmark | What | |---|---| | `RouteLookup` | the routing-table lookup every request pays | | `ServeProxy` | full request pipeline to a local backend (in-process) | | `ServeProxyAccessList` | same, with an IP access list evaluated per request | | `ProxyThroughput` | end-to-end requests/sec over a real loopback socket | | `CacheHitThroughput` | same, but responses served from the per-host cache | ## Results Measured on an **AMD Ryzen 7 9800X3D** (8 cores / 16 threads), Go 1.25, loopback, no TLS: | Benchmark | ns/op | ≈ requests/sec | allocs/op | |---|---:|---:|---:| | Route lookup | 9.3 | ~108,000,000 | 1 | | Proxy to local backend | 19,500 | ~51,000 | 105 | | Proxy + IP access list | 17,500 | ~57,000 | 105 | | Proxy throughput (real socket) | 22,000 | ~45,000 | 158 | | Cache hit | 5,500 | ~180,000 | 71 | How to read them: - **Routing is cheap.** A host lookup is ~9 ns and one allocation, and it is a map lookup, so it does not grow with the number of hosts you configure. - **Access lists are free.** Adding an IP access list moves throughput by less than run-to-run noise. - **~45,000 requests/sec proxied** end-to-end with the load generator, quicgate and the backend all fighting for the same 8 cores. Move the client and backend off-box and quicgate keeps the cores to itself. - **Cache hits are ~4× faster** (~180,000/sec) because the backend is bypassed entirely. ## Real-world load test For end-to-end numbers on your deployment (real network, TLS, concurrency), point a load generator at a running instance proxying to a fast static backend: ```bash # e.g. bombardier (https://github.com/codesenberg/bombardier) bombardier -c 200 -d 30s https://app.example.com/ # or wrk (https://github.com/wg/wrk) wrk -t8 -c200 -d30s https://app.example.com/ ``` Keep the backend trivial (a static file or a tiny "ok" responder) so you measure quicgate, not the app behind it. Run the load generator from a *different* machine than quicgate for a true server-capacity number. HTTP/3 (UDP 443) needs a client that speaks h3. ## Real-world (live homelab) Measured against the live deployment: one LAN client -> quicgate on a **6-vCPU homelab VM that also runs ~50 other containers** -> a static-file backend serving a real **90 KB** page, over real TLS (HTTP/2), LAN-direct. | Test | Result | |---|---| | GET the 90 KB page (200) | **566 req/s, 51 MB/s (~408 Mbit/s)**, p50 70 ms, p99 79 ms | | Small responses over TLS, single client | **~5,000 req/s**, p50 7 ms, p99 19 ms | | Pinned to one fast core, TLS, small real backend | **~8,900 req/s**, p99 11 ms | Page-serving throughput stayed **flat at ~566 req/s from 40 to 200 concurrent connections** while latency grew linearly (70 -> 177 -> 354 ms) -- the signature of a fixed pipe *upstream* of the proxy. Here that pipe is the **single client's ~400 Mbit/s link**, not quicgate, which rejected small requests at 5,000+/s and sustained ~8,900 TLS-proxied req/s on a single core. Serving real traffic it saturated the available bandwidth with CPU to spare; its true ceiling needs several off-box clients (one machine maxes its own link first). ## How it compares quicgate's data plane is Go `net/http` + `httputil.ReverseProxy`. In independent third-party benchmarks, the major reverse proxies land within a small factor of one another -- all comfortably in the tens of thousands of requests per second on real hardware, separated more by payload size, TLS and the backend behind them than by the proxy itself. For any modern reverse proxy running under ~10,000 requests per second per instance -- which covers essentially every self-hosted and small-fleet deployment -- the choice is **not throughput-bound**. quicgate's own measured numbers (above) land squarely in that range and hold their own against every major proxy for the loads a homelab or small fleet will ever put on one: - routing is effectively free (**~9 ns** per lookup), - a **single fast core** sustains **~8,900** TLS-proxied req/s, - a dedicated 8-core box does **~45,000** proxied / **~180,000** cached req/s, - and in the live deployment the bottleneck was the client's network link, not the proxy. Benchmarks vary enormously by CPU, payload, TLS and backend, so the only number that matters for your setup is the one you measure: run `go test -bench=. ./internal/engine` on your own hardware.