Files
RustProxy/docs/PERFORMANCE.md
T
2026-11-06 23:16:36 +01:00

7.2 KiB
Raw Blame History

Performance

RustProxy runs on Tokio and Hyper with an asynchronous, streaming request path. This guide describes how performance is measured, where RustProxy stands compared with a similarly configured Nginx, and which settings matter in production.

What is measured

Results are only comparable when both servers do the same work. A stock Nginx installation does very little per request, while RustProxy always records request statistics, traffic analytics and access logs and adds forwarding headers. The comparison therefore uses an Nginx configuration with a similar feature set:

Feature Nginx RustProxy
Access log log_format with timing fields, 64 KiB buffer Built-in access log and live statistics (default)
Forwarding headers Host, X-Forwarded-For, X-Forwarded-Proto, X-Real-IP Forwarded, X-Forwarded-*, X-Real-IP, X-Request-Id (default)
Response headers X-Request-ID, X-Content-Type-Options Request ID and default security headers
Upstream keep-alive keepalive 64, HTTP/1.1 Connection pool (default)
Cache proxy_cache with proxy_cache_lock HOT memory cache with request collapsing
Compression gzip on gzip/Brotli/Zstandard negotiation (default)
Static files root, etag on Web Service with validators (default)

Method

  • Machine: 2 vCPU virtual machine, Linux 6.18, loopback networking.
  • Each server is pinned to one CPU core (worker_processes 1 for Nginx, taskset for RustProxy). The load generator and the origin share the second core.
  • Load: wrk -t1 -c50 -d8s, HTTP/1.1 keep-alive, two or three runs per scenario, average reported.
  • Origin: Nginx serving static files with Cache-Control: public, max-age=600.
  • Nginx 1.24 and RustProxy release builds (lto = "thin", codegen-units = 1).

Results

Requests per second on one CPU core (higher is better):

Scenario Nginx RustProxy 0.35.0 RustProxy 0.35.2 0.35.2 vs 0.35.0
Reverse proxy, 1 KiB response 61,400 14,700 15,100 +3%
Reverse proxy, 100 KiB response 10,600 14,500 15,500 +7%
Cache hit, 1 KiB response 53,300 22,500 25,000 +11%
Web Server, 1 KiB file 85,000 15,200 26,000 +71%
Web Server, 100 KiB file 25,900 9,200 16,900 +84%

Reading the results:

  • Large proxied responses: RustProxy streams the body directly between connections and is faster than Nginx in its default buffering mode, which spools larger upstream responses to temporary files.
  • Small requests: Nginx remains ahead, because its fixed per-request cost is lower. In RustProxy this cost is dominated by per-request accounting (live statistics, traffic analytics, request IDs and logs) and by the asynchronous runtime. For most deployments it is far below the capacity of the backends behind the proxy.
  • Static files: since 0.35.1 RustProxy reads files directly on the request task instead of a background thread pool, which nearly doubles static throughput.
  • Why small requests and cache hits trail Nginx: each proxied request crosses the loopback network twice (client and backend), and on a VM about half of the CPU time goes to the kernel for those writes and the wake-ups of the other side. Most of the remaining time is RustProxy's per-request work that Nginx does not do in the same way: request IDs, live per-Vhost statistics, traffic analytics and structured access-log entries, plus the asynchronous runtime. A cache hit saves the backend hop but still pays for this accounting, so its gap is similar. No single function dominates the profile; closing the gap further would require architectural changes rather than tuning.
  • CPU per request: at the 1 KiB proxy scenario RustProxy uses about 70 µs of CPU time per request on the test machine, Nginx about 17 µs. A single core therefore sustains well over 10,000 proxied requests per second.

Throughput scales with additional CPU cores; RustProxy uses all available cores automatically.

Memory allocator

Release builds use the operating system's allocator (glibc on most Linux distributions). An alternative allocator, mimalloc, can be compiled in as an optional feature:

cargo build --release --features mimalloc

It is off by default because of how it behaves with a large HOT cache. Measured on the test machine with a 300 MiB HOT cache under constant turnover (random objects of 1–260 KiB, continuous evictions):

Build Process memory (RSS) Throughput under cache turnover
System allocator (default) 614 MiB 5,700 req/s
mimalloc 760 MiB (+24%) 20,300 req/s (3.5×)
mimalloc with MIMALLOC_PURGE_DELAY=0 720 MiB (+17%) 18,600 req/s

In the standard scenarios from the results table (proxy, cache hits, static files) the two allocators performed within measurement noise. On a production server with a 600 MiB HOT cache the same difference showed as about 800 MiB versus 1.1–1.2 GiB of process memory.

Choose mimalloc only when all of these apply:

  • the HOT cache holds many objects that are replaced constantly (high eviction rate on the Cache page);
  • throughput under that turnover matters more than memory;
  • the server has at least 30% RAM headroom above the configured HOT cache size.

Otherwise keep the default. The system allocator returns memory from evicted cache entries to the operating system right away, so process memory stays close to the configured cache size.

Production tuning

Setting Recommendation
Cache Enable the HOT cache for cacheable content; hits avoid the backend completely.
Compression Keep it enabled for text content. Use precompressed .br/.gz files for static sites.
Upstream protocol Prefer HTTP/1.1 keep-alive or HTTP/2 to backends; avoid backends that close every connection.
Health checks Use intervals of several seconds; frequent probes consume capacity on large installations.
Logging Keep stored log files on fast local storage and use retention limits on busy servers.
Limits --max-connections (default 512) and --max-inflight (default 256) cap concurrency. Raise them on busy servers with enough memory.
sendfile Optional for large cleartext static files on Linux. On the test machine it gave no measurable gain, so keep off unless your own measurements show otherwise.
Memory allocator Keep the default system allocator. See Memory allocator for when the optional mimalloc build is worth its extra memory.
TLS TLS 1.3 with session resumption keeps handshake cost low; HTTP/2 or HTTP/3 reduce connection setup for browsers.

Reproducing the test

  1. Start an origin (any static HTTP server) with a 1 KiB and a 100 KiB file.
  2. Configure RustProxy with one Vhost pointing to the origin and one Web Service serving the same directory.
  3. Configure Nginx with the equivalent features from the table above.
  4. Pin each server to the same number of cores and run wrk from a different core:
taskset -c 1 wrk -t1 -c50 -d8s -H "Host: proxy.example" http://127.0.0.1:8080/1k.txt

Always compare matched configurations on the same machine, and repeat each run. Results on physical hardware and real networks differ from loopback measurements.