From b8a125b598cad7d305c193532b17d1975cf37780 Mon Sep 17 00:00:00 2001 From: Razvan Dimescu Date: Wed, 22 Apr 2026 23:30:55 +0300 Subject: [PATCH] fix(upstream): default hedge_ms=0 to avoid silent 2x upstream query count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hedging fires a second upstream query against the same upstream after the hedge delay. Rescues packet loss and handshake stalls on flaky links, but every lookup shows up twice at the provider — silently halves the headroom for anyone on a quota'd upstream (NextDNS free tier, Control D, paid Quad9). Surfaced by #134 (bcookatpcsd), who saw every query duplicated on the NextDNS dashboard with a single-address DoT upstream. Not a bug — the feature doing what it says on the tin — but a surprising default. Flipping the default to 0 makes hedging explicitly opt-in. Users who want tail-latency rescue on flaky nets add `hedge_ms = 10` (or higher). No config migration needed; no breaking changes to the API surface. Also tightens the numa.toml comment so the trade-off is visible at config time, not retroactively on a provider dashboard. --- numa.toml | 12 +++++++----- src/config.rs | 6 +++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/numa.toml b/numa.toml index 93418ea..baf35aa 100644 --- a/numa.toml +++ b/numa.toml @@ -30,11 +30,13 @@ api_port = 5380 # fallback = ["8.8.8.8", "1.1.1.1"] # tried only when all primaries fail # port = 53 # default port for addresses without :port # timeout_ms = 3000 -# hedge_ms = 10 # request hedging delay (ms). After this delay -# # without a response, fires a parallel request -# # to the same upstream. Rescues packet loss (UDP), -# # dispatch spikes (DoH), TLS stalls (DoT). -# # Set to 0 to disable. Default: 10 +# hedge_ms = 0 # request hedging delay (ms). Default: 0 (off). +# # Set to e.g. 10 to fire a parallel upstream +# # request after 10ms of silence — rescues packet +# # loss (UDP), dispatch spikes (DoH), TLS stalls +# # (DoT). Doubles the upstream query count, so +# # leave off for quota'd providers (NextDNS, +# # Control D). # ODoH (Oblivious DNS-over-HTTPS, RFC 9230). The relay sees your IP but # not the question; the target sees the question but not your IP. Numa diff --git a/src/config.rs b/src/config.rs index 6daf430..f28d647 100644 --- a/src/config.rs +++ b/src/config.rs @@ -451,8 +451,12 @@ fn default_upstream_port() -> u16 { fn default_timeout_ms() -> u64 { 5000 } +/// Off by default: hedging fires a second upstream query, which silently +/// doubles the count at the provider — hurts quota'd DNS (NextDNS, Control +/// D). Opt in with `hedge_ms = 10` for tail-latency rescue on flaky nets +/// or handshake-slow DoT. fn default_hedge_ms() -> u64 { - 10 + 0 } #[derive(Deserialize)]