Skip to content
Oday Bakkour
Back to Knowledge Hub

How Four Fixes Took ReverseDock From Half of NGINX to Level

Oday Bakkour profile photo
Oday Bakkour
5 min read
Share
How Four Fixes Took ReverseDock From Half of NGINX to Level

Executive Summary

  • The problem: on 2 CPUs, ReverseDock peaked at 11,839 req/s with a single backend, with 2.7 % failed. NGINX did 24,889.
  • The hunt: perf, strace -c and the container's cpu.stat found four costs. Each got its own pull request and its own measurement.
  • The result: 24,619 req/s, level with NGINX, and no failed requests in any scenario at any rate. CPU per request went from 139 to 76 µs.
  • Still behind: HAProxy is about 30 % faster on plain proxying, and NGINX uses less CPU per request.
  • Status: this is main, the v0.5 release candidate, and ReverseDock is still an alpha. All numbers come from the published report.

I made a short video of this on X.

Half of NGINX's speed on 2 CPUs

Half of NGINX's speed on 2 CPUs

The lab

Every proxy runs alone in a container limited to 2 CPUs and 1 GiB, with the backend and k6 on 4 CPUs each. It is k6's open model, with a 10 s warm-up and 30 s at each target rate, on one dedicated Hetzner server (8 vCPUs, 32 GiB). The responses are 1 KiB unless noted. NGINX is 1.29.3 and HAProxy is 3.2.8, as pinned in the lab's compose file.

You can run it yourself from the benchmarks folder:

terminal
for s in single lb tls tls-new slow routes; do
  SCENARIO=$s VERSION=2026-11-02-small-cpu ./run.sh
done
Before the fixes: half of NGINX's peak

Before the fixes: half of NGINX's peak

The four costs

1. A 1,024 open-file limit (#170). The container's soft limit capped ReverseDock near 500 requests in flight, so every stall became errors. Raising it to the hard limit at start saved no CPU, but the 1–3 % failures at the 40,000 req/s target went to none.

2. musl's allocator (#172). It mapped and unmapped memory for every 64 KiB buffer and took a global lock, and perf put about 27 % of CPU there. With mimalloc as the global allocator, CPU per request went from 139 to 90 µs on its own.

3. Syscalls on the hot path (#173). There was a getrandom call per request for the request id, and an access-log record built even when the log was off. Now the UUIDv7 ids come from a thread-local generator and the record is skipped when nobody reads it. This one was only measured in the combined branch, so I have no separate number for it.

4. Two TCP packets per response (#174). Pingora's stream wrote the response head and body in two sendto calls, so the client ACKed twice. An 8 KiB write buffer flushed at the end of the response sends one segment, for about 8 % less CPU.

CPU per request at 5,000 req/s, from cpu.stat:

  • Before: 139 µs
  • Open-file limit alone: 151 µs (noise: this fix saves no CPU)
  • mimalloc alone: 90 µs
  • Write buffer alone: 128 µs
  • All four, main now: 76 µs
  • NGINX: 47 µs

The profiling, the four pull requests and their merges all happened on November 2. The issue had been open since October 30.

Four costs, four pull requests, 139 to 76 µs

Four costs, four pull requests, 139 to 76 µs

The result

Peak completed req/s on 2 CPUs, counting only runs with under 5 % failed:

  • Single backend: 11,839 before, 24,619 now. NGINX 24,889, HAProxy 31,895.
  • Load balancing: 10,000 before, 24,094 now. NGINX 24,986, HAProxy 31,960.
  • TLS, reused connections: 10,091 before, 22,326 now. NGINX 21,600, HAProxy 29,991.
  • TLS, new connections: 4,999 before, 5,821 now. NGINX 3,815, HAProxy 3,227.
  • Slow backend (100 ms): 3,698 before, 17,755 now. NGINX 9,556, HAProxy 19,091.
  • Many routes: 10,673 before, 23,075 now. NGINX 24,221, HAProxy 28,106.

Level means level here: run-to-run noise is about 10 %, and NGINX's single-backend peak was 22,638 in one session and 24,889 in the next. I don't read a difference of a few percent as a win for anyone.

Level with NGINX on a single backend

Level with NGINX on a single backend

Under overload

At the 40,000 req/s target, ReverseDock and HAProxy failed no request in any of the six scenarios, while NGINX failed between 1.10 % and 3.76 %.

Failed requests at the 40,000 req/s target

Failed requests at the 40,000 req/s target

Where it pulled ahead

  • New TLS connection per request: 5,821 req/s, against NGINX's 3,815 (1.5×) and HAProxy's 3,227 (1.8×). This is the one scenario where it beats HAProxy.
  • Slow backend: 17,755 req/s against NGINX's 9,556 (1.9×).
  • 100 KiB responses at 10,000 req/s (1 GB/s): the only one of the five that keeps up on 2 CPUs. NGINX completes 7,470 and HAProxy 7,460.
Three scenarios where ReverseDock pulled ahead of NGINX

Three scenarios where ReverseDock pulled ahead of NGINX

Where it still loses

  • HAProxy on plain proxying: ReverseDock is at 0.75–0.82× of its peak, 0.77× with a single backend. The remaining cost looks like cross-thread wake-ups in the runtime, and #176 tracks a thread-per-core experiment.
  • NGINX on CPU: 76 µs per request against 47 µs at 5,000 req/s.
  • Latency at moderate rates: the p99 at 5,000 req/s is 0.46 ms against NGINX's 0.35 ms and HAProxy's 0.37 ms.
  • The TLS tail: the p99.9 at 10,000 req/s with TLS is 30.7 ms against NGINX's 4.8 ms.
  • Not measured: 4 and 8 CPUs. This is one machine, one lab and 2 CPUs, so other hardware gives other numbers.
What is still behind and still unknown

What is still behind and still unknown

What's next

The v0.5 tag comes after a 24-hour soak on the release candidate passes. The full numbers and method are in the results folder and on the performance page of the docs. If you'd have looked somewhere else first, tell me on X or open a discussion.

Sources

Comments

Share your thoughts and join the conversation

Leave a Comment

Loading comments...
RELATED