Head-to-head: queries per second

Cached query throughput, 10-second runs, medians of 3. Both servers on all 24 cores. Zero packet loss on both.

rDNS, 50 clients
639,581
Unbound, 50
596,564
rDNS, 100
621,983
Unbound, 100
621,024
rDNS, 200
605,527
Unbound, 200
615,914
rDNS, 500
565,544
Unbound, 500
609,617

Average latency

Under peak load, Unbound holds a latency edge; rDNS's advantage is single-client latency (see notes).

ClientsrDNSUnbound
5098 µs76 µs
100110 µs70 µs
200107 µs74 µs
500116 µs79 µs

Test environment

CPU24 cores (AMD64)
RAM32 GB
OSLinux 6.6.87 (WSL2)
rDNSv1.17.17, release build (LTO fat, codegen-units=1, target-cpu=native)
Unbound1.19, num-threads: 24, so-reuseport: yes, module-config: iterator
Workload100 unique queries (A, AAAA, MX, NS, TXT, NXDOMAIN), all cached
Tooldnsperf, 12 sender threads, 10s runs, medians of 3

Both servers configured as forwarders to 1.1.1.1 with DNSSEC disabled, logging at error-only level.

The optimization journey

rDNS started at 29,630 QPS and reached 437,434 across five rounds, then a sixth profiling round took it to ~640,000. That sixth round is the most instructive: a fair benchmark against multi-threaded Unbound showed rDNS behind, and perf traced ~33% of per-query CPU to allocation and SipHash. Each round is a learning artifact for any high-performance Rust networking work.

v6: kill per-query allocation and hashing (+22%)

Cache hits were paying for a throwaway Vec<String> per name-compression probe, a double SipHash on every cache key, a deep clone of the whole cache entry, and a full re-encode (with fresh compression) of a response that never changes.

Fix: borrow instead of allocating in the compression probe; a small inline FxHash for the cache and compression map (hashing the shard key once); Arc-shared cache entries; and — the biggest win — precompute each response's wire body once and memoize it on the entry, so hits just copy it and patch the TTL fields. Byte-identical output; retuned the recv-worker count too.

v1 → v2: Concurrency (+216%)

The original UDP listener processed queries sequentially. Each query blocked the socket while waiting for upstream resolution.

Fix: Spawn a Tokio task per incoming query. Forwarder connection pool multiplexes queries over one connected UDP socket with oneshot-channel response dispatch.

v2 → v3: Reduce allocations (−9%, better scaling)

Spawning a task per query added clone overhead and full Message::decode → encode round trips.

Fix: Sync fast-path for cache hits, authoritative answers, and RPZ blocks — inline in the recv loop. Wire-format fast parser. Direct wire encode with TTL adjustment.

v3 → v4: Faster cache (+264%)

DashMap's coarse sharding and get_mut write-lock-on-hit became the bottleneck.

Fix: Custom 256-shard cache on parking_lot::RwLock. Cache hits take read locks only. LTO fat, single codegen unit, target-cpu native in the release profile.

v4 → v5: Eliminate socket contention (+41%)

Multiple workers sharing one socket caused kernel-level race on the receive buffer.

Fix: SO_REUSEPORT — separate socket per worker on the same port. Kernel distributes packets by flow hash. SO_RCVBUF increased to 4 MB.

Reproduce it

# Build with native CPU optimizations
RUSTFLAGS="-C target-cpu=native" cargo build --release

# Install tools
sudo apt-get install -y dnsperf unbound

# Run the benchmark suite
bash bench/run.sh

# Or manually:
./target/release/rdns -c bench/rdns-bench.toml &
dnsperf -s 127.0.0.1 -p 5553 -d bench/queryfile.txt -c 50 -l 10 -Q 500000

Notes

  • Unbound runs multi-threaded (num-threads: 24, so-reuseport: yes) — a fair all-cores-vs-all-cores test. Earlier revisions compared against single-threaded Unbound and are superseded by these numbers.
  • Single-client throughput is lower than Unbound because SO_REUSEPORT distributes by flow hash — one source, one worker. Not a realistic production scenario, but it means single-client latency stays very low.
  • Medians of 3 runs with a co-located load generator, so ±5–10% variance is expected — read the result as parity, not a precise multiplier.
  • These benchmarks measure cached query throughput only. Cold-cache depends on upstream latency.
  • Results vary by hardware, kernel, and system load.

Get rDNS running in 60 seconds.

Single static binary. TOML config. MIT licensed. Linux, FreeBSD, and macOS.