> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dnzwithazee.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Proxy benchmarks

> How fast dnz-proxy forwards requests compared with nginx, HAProxy, Envoy, Caddy and Traefik on one CPU, how much memory each holds, and how to measure it yourself.

[dnz-proxy](https://docs.dnzwithazee.com/proxy/dnz-proxy.md) was measured against the reverse proxies people run (nginx, HAProxy,
Envoy, Caddy and Traefik), each pinned to one CPU in front of the same origin.

## Results

Measured on 2026-10-24 on a dedicated server: a KVM virtual machine with 4 CPUs (Intel Xeon Silver
4114, 2.2 GHz), x86\_64, nothing else running. Each proxy pinned to **one CPU**, the origin to
another, the load generators to the other two. Requests per second are the median of 3 runs of 20 s
with 64 connections.

| Proxy | HTTP, 1 KiB | HTTPS, 1 KiB | HTTPS, new connection per request | HTTP, 64 KiB |
| - | -: | -: | -: | -: |
| nginx 1.30.6 | 24,624 | 17,772 | 1,356 | 6,769 |
| HAProxy 3.4.7 | 22,999 | 17,803 | 1,238 | 5,872 |
| **dnz-proxy 1.7.1** | **11,668** | **11,476** | **2,441** | **7,216** |
| Traefik 3.7.16 | 8,586 | 8,097 | 1,433 | 5,128 |
| Envoy 1.40.1 | 8,584 | 8,010 | 1,930 ¹ | 5,487 |
| Caddy 2.11.7 | 6,692 | 6,211 | 1,336 | 4,565 |

At a steady 5,000 requests a second over HTTP, one every 0.2 ms (well below every proxy's limit):

| Proxy | Latency p50 | Latency p99 | CPU per request | Memory idle | Memory loaded |
| - | -: | -: | -: | -: | -: |
| nginx | 0.44 ms | 1.38 ms | 75 µs | 17 MiB | 21 MiB |
| HAProxy | 0.40 ms | 1.25 ms | 68 µs | 17 MiB | 21 MiB |
| **dnz-proxy** | **0.60 ms** | **1.54 ms** | **100 µs** | **2 MiB** | **8 MiB** |
| Traefik | 0.83 ms | 13.16 ms | 137 µs | 13 MiB | 42 MiB |
| Envoy | 1.22 ms | 16.54 ms | 152 µs | 14 MiB | 23 MiB |
| Caddy | 1.28 ms | 40.32 ms | 163 µs | 9 MiB | 21 MiB |

dnz-proxy is the release image `terrificci/dnz:1.7.1`. The origin alone answers 36,883 requests a
second for 1 KiB and 30,139 for 64 KiB, so every number above is the proxy's own limit; on its own
it answers the steady load in 0.3 ms (p99 1.0 ms). No run had a failed or non-200 answer.

1. Envoy didn't resume TLS sessions in the harness's check, so its new connections are full
   handshakes; the other five resumed.

What it shows:

* **nginx and HAProxy** forward keep-alive HTTP about twice as fast as dnz-proxy and HTTPS about 1.5
  times as fast, and use 68 to 75 µs of CPU per request against dnz-proxy's 100 µs.
* **dnz-proxy** comes third on keep-alive HTTP and HTTPS, ahead of Traefik, Envoy and Caddy, and
  first on 64 KiB answers and on new TLS connections (rustls with *ring*, against OpenSSL in nginx
  and HAProxy).
* **Latency**: nginx and HAProxy add about 0.1 ms to the origin's 0.3 ms, dnz-proxy 0.3 ms; all
  three keep p99 near 1.5 ms. Traefik, Envoy and Caddy have p99s of 13 to 40 ms.
* **Memory**: dnz-proxy holds the least, 2 MiB idle and 8 MiB under load, against 21 to 42 MiB for
  the others.

> [!NOTE]
>
> Until 2026-10-24 this page measured latency with `hey`, whose workers send in step: up to 50
> requests arrived at once, so the latency mostly showed how long a proxy takes to work through 50
> requests (dnz-proxy 1.6.1: 3.5 ms p50). The harness now spaces requests evenly with `oha`.

## After 1.7.0: one write per answer, fewer allocations

A profile of dnz-proxy under this load (`perf`, 1 CPU) showed no hot spot in dnz-proxy's own code,
but nearly half the CPU in the kernel's network path, and two causes worth fixing:

* **Each answer's header and body went out in two writes.** Pingora gives a connection a write
  buffer of one TCP segment only in its own listeners; connections from dnz-proxy's listener had
  none, so a 1 KiB answer took two packets, each a full trip through the TCP stack. Now they leave
  in one. Event streams and WebSockets aren't held back: each chunk and each message on an upgraded
  connection is still sent right away.
* **Every visit built a whole request log line** (a dozen allocations, a GeoIP lookup), though only
  **Logged per second, per node** of them are kept. The traffic counters now take what they need
  from the visit, and a line is made only when it is kept.

Measured against 1.7.0 (its dnz-proxy is the same as 1.6.1's) on the same server, both builds taking
turns, median of 4 rounds:

| | 1.7.0 | After | Change |
| - | -: | -: | -: |
| HTTP, 1 KiB | 10,699 | 12,006 | +12% |
| HTTPS, 1 KiB | 9,777 | 11,531 | +18% |
| HTTPS, new connection per request | 2,331 | 2,429 | +4% |
| HTTP, 64 KiB | 6,911 | 7,158 | +4% |
| Latency p50 / p99 at 5,000/s ² | 3.5 / 6.7 ms | 3.2 / 5.5 ms | |
| CPU per request | 108 µs | 94 µs | −13% |
| Memory idle / loaded | 2 / 8 MiB | 2 / 8 MiB | same |

These changes are in 1.7.1, the release in the tables above.

2. With the old bursty load (`hey`, see the note above).

## Method

* Each proxy runs in its own container on one Docker network, started on its own, and forwards to
  the same origin: nginx serving two static files (1 KiB and 64 KiB) on its own CPU.
* **CPUs are pinned** (`cpuset`), not limited by a quota, so each proxy sizes its worker threads to
  the one CPU it may use, as it would on a one-CPU server. Envoy, which counts the host's CPUs,
  gets `--concurrency 1`.
* **dnz-proxy** joins a DNZ that pushes it the routers, the service and the certificate over its
  configuration stream, as in production. It runs with its defaults: analytics and request logs on.
  The DNZ control plane shares the origin's CPU; no traffic goes through it.
* **The others** run the official images with a minimal configuration (in `proxybench/conf/`): one
  route to the origin, upstream keep-alive, the usual `X-Forwarded-*` headers, nginx's access log
  off (the others don't log requests by default).
* TLS is TLS 1.3 with the same ECDSA P-256 certificate everywhere. Each proxy keeps its default
  session resumption; wrk resumes sessions on new connections, so the new-connection column is
  mostly TCP setup and resumed handshakes.
* **Load**: [wrk](https://github.com/wg/wrk) with 2 threads and 64 connections for the requests per
  second (HTTP/1.1; `Connection: close` for the new-connection case), and
  [oha](https://github.com/hatoo/oha) for the steady rate: one request every 1/5,000 s over 50
  connections, each request's latency counted from when it was due (`--latency-correction`), after a
  2 s warm-up.
* **CPU per request** is the proxy's CPU time (its cgroup's `cpu.stat`) during the steady-rate run
  divided by the requests, so it includes whatever the proxy does in the background. **Memory** is
  the cgroup's anonymous memory, idle after start and right after the load; it leaves out the binary
  itself.

## Run it yourself

The harness is in [`proxybench/`](https://github.com/terrific-ci/dnz/tree/main/proxybench) on the
public repository. It needs Docker and runs DNZ and dnz-proxy from the release image on Docker Hub
(`terrificci/dnz:latest`), next to the other proxies' official images:

```bash theme={null}
curl -fsSL https://github.com/terrific-ci/dnz/archive/refs/heads/main.tar.gz \
  | tar -xz --strip-components=1 dnz-main/proxybench
cd proxybench
./run.sh
```

It needs at least 4 CPUs: the proxy under test, the origin and the load generators each get their
own.

| Variable | Default | What it sets |
| - | - | - |
| `DNZ_IMAGE` | `terrificci/dnz:latest` | The DNZ image (DNZ and dnz-proxy), e.g. `terrificci/dnz:1.7.1` |
| `BENCH_PROXIES` | all | Only these, e.g. `"dnzproxy nginx"` |
| `BENCH_DURATION`, `BENCH_RUNS` | `20`, `3` | Seconds per run, and runs per scenario |
| `BENCH_CONNECTIONS` | `64` | wrk's connections |
| `BENCH_RATE` | `5000` | Requests a second in the steady-rate run |
| `BENCH_PROXY_CPUS` | `1` | CPUs for the proxy under test (a cpuset list, e.g. `1,2`) |
| `BENCH_BACKEND_CPUS`, `BENCH_PERF_CPUS` | `0`, `2,3` | CPUs for the origin and the load generators |
| `BENCH_KEEP` | off | Leave the containers running afterwards |

```bash theme={null}
BENCH_DURATION=60 BENCH_RUNS=5 BENCH_PROXIES="dnzproxy nginx" ./run.sh
```

`run.sh` prints the results table for your machine with the versions it ran; the raw wrk and oha
output is in `work/raw/`. For the DNS server benchmarks, see [Benchmarks](https://docs.dnzwithazee.com/deploy/benchmarks.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.