Benchmarks
~640,000 cached queries per second. On par with multi-threaded Unbound.
rDNS measured against a fully multi-threaded Unbound 1.19 on the same hardware, with the same workload, using dnsperf. All cores vs all cores — everything is reproducible.
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.
Average latency
Under peak load, Unbound holds a latency edge; rDNS's advantage is single-client latency (see notes).
| Clients | rDNS | Unbound |
|---|---|---|
| 50 | 98 µs | 76 µs |
| 100 | 110 µs | 70 µs |
| 200 | 107 µs | 74 µs |
| 500 | 116 µs | 79 µs |
Test environment
| CPU | 24 cores (AMD64) |
|---|---|
| RAM | 32 GB |
| OS | Linux 6.6.87 (WSL2) |
| rDNS | v1.17.17, release build (LTO fat, codegen-units=1, target-cpu=native) |
| Unbound | 1.19, num-threads: 24, so-reuseport: yes, module-config: iterator |
| Workload | 100 unique queries (A, AAAA, MX, NS, TXT, NXDOMAIN), all cached |
| Tool | dnsperf, 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_REUSEPORTdistributes 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.