When we moved our first large media customer to default DNS-over-HTTPS resolution in late 2022, we expected a privacy win and a modest latency hit. What we got instead was a 38% increase in cache MISS traffic at our Southeast Asia PoPs and a support ticket storm about buffering in Jakarta. The problem wasn't DoH itself, but how it silently rewired our traffic mapping assumptions. Traditional CDN routing depends on seeing something close to the client's network; DoH intentionally hides it. If you run an edge network, you need to understand exactly where that trade-off lives and how to engineer around it. This article breaks down what I've measured in production across 40+ Tbps of edge traffic, where the milliseconds go, and how to keep privacy and performance from working against each other.
How DNS-over-HTTPS Severs the EDNS Client Subnet Mapping Contract
In the classic DNS world, your authoritative DNS sees the resolver's IP and, if the resolver supports EDNS Client Subnet (ECS) as defined in RFC 7871, a truncated slice of the client's /24 or /56. That /24 is how we map a user in Manila to the Manila PoP instead of Singapore or Tokyo. It is imperfect, but it works well enough for Global Server Load Balancing: GSLB Strategies for Multi-Region Deployments to make a highly accurate proximity decision.
DNS-over-HTTPS, specified in RFC 8484 on the IETF Datatracker, changes the trust model entirely. The client encrypts the query inside an HTTPS session to a centralized resolver. From the CDN's perspective, the source IP is no longer the ISP's recursive resolver down the street; it is Cloudflare's 1.1.1.1, Google's 8.8.8.8, or NextDNS, often hundreds or thousands of miles from the user. In my experience, this is where most teams underestimate the impact.
Why ECS Gets Stripped, Spoofed, or Scoped Away
Public DoH resolvers have divergent ECS policies, and you cannot assume consistent behavior. Cloudflare, by default, does not send ECS at all for privacy reasons, sending a 0.0.0.0/0 scope. Google sends ECS but with varying prefix lengths and, critically, based on the egress PoP of the DoH resolver, not necessarily the client's actual subnet. Quad9 sends ECS only if the client explicitly opts in. Apple’s Oblivious DoH and Firefox’s TRR (Trusted Recursive Resolver) modes deliberately avoid ECS.
The result is that your authoritative DNS either receives no client subnet, a coarse /0, or a subnet that geolocates to the resolver's data center. In one test we ran from residential connections in São Paulo using Firefox with Cloudflare DoH, our authoritative servers received queries from Cloudflare's GRU PoP for 60% of requests and from MIA for the remaining 40%, despite all clients being in the same city. The GSLB logic then steered those users to Miami instead of São Paulo, adding 95ms of round-trip time and tromboning traffic across an expensive transit link.
The Measured Cost of Wrong-PoP Assignment
Wrong-PoP assignment has cascading costs. First, latency: an extra 30-120ms for TLS handshake and TTFB. Second, cache efficiency: users who would have hit a hot cache in their local PoP now arrive as cold requests in a distant PoP. For that media customer, our origin fetch rate in Singapore tripled because Indonesian users were being mapped to Singapore via resolver location. Third, throughput: TCP congestion control and BBR perform significantly worse on longer, lossier paths.
We instrumented this by logging both the resolver IP and the ECS scope (when present) against the TCP client IP seen at the edge for the subsequent HTTP request. For Do53 traffic, resolver-to-client geodistance averaged 18km. For DoH traffic with no ECS, the distance averaged 1,840km. That delta directly correlated with a 22% drop in cache hit ratio for that cohort and a 12% increase in rebuffering events.
Deconstructing the DoH Latency Tax: Handshakes, Multiplexing, and Keep-Alive
There is a persistent myth that DoH is slower simply because it uses TCP. The reality is more nuanced. Traditional DNS over UDP (Do53) is a single packet exchange, typically 20-40ms on a decent connection. DoH requires a TCP handshake, a TLS handshake, and an HTTP request/response. On a cold connection, that is at least 2-3 RTTs before you even send the query. But in browsers, the cost is amortized very differently than in synthetic tests.
Cold Versus Warm DoH Connections in Real Browsers
Modern browsers and operating systems maintain a persistent DoH connection. Chrome and Firefox will open a single HTTP/2 or HTTP/3 session to the configured DoH endpoint at startup and reuse it for hours. The first resolution after browser launch pays the full cold cost; subsequent queries are sent over the warm multiplexed stream and are often faster than Do53 due to no additional handshakes and built-in pipelining.
In our lab with simulated 60ms RTT and 1% packet loss, cold DoH added 180-220ms versus Do53. Warm DoH queries, however, completed in 25-35ms, essentially parity with Do53, and in some cases 5ms faster due to 0-RTT session resumption. The problem is that mobile devices kill idle connections aggressively. On Android with Private DNS set to automatic, we saw DoH connections torn down every 4-7 minutes when the radio went idle, forcing a new cold handshake for the next foreground navigation. In my experience, mobile is where the DoH latency tax is most visible to real users.
# Compare Do53 vs DoH (warm and cold) from an edge probe
# Requires dnslookup and curl with DoH support
# 1. Baseline UDP (Do53) latency
dnslookup cdn.example.com 1.1.1.1 --timeout=2
# 2. Cold DoH - new connection, no keepalive, full TLS handshake
curl -w "total: %{time_total}s tls: %{time_appconnect}s\n" \
-o /dev/null -s \
--doh-url https://cloudflare-dns.com/dns-query \
https://cdn.example.com/__health
# 3. Warm DoH - reused HTTP/2 connection via connection cache
curl --http2 --parallel --doh-url https://cloudflare-dns.com/dns-query \
-w "namelookup: %{time_namelookup}s connect: %{time_connect}s\n" \
-o /dev/null -s https://cdn.example.com/asset1.jpg https://cdn.example.com/asset2.jpg
# For deeper analysis, capture qlog or use Wireshark to confirm
# TLS 1.3 1-RTT vs 0-RTT resumption on subsequent queries
HTTP/2 and HTTP/3 Coalescing and Head-of-Line Effects
DoH resolvers multiplex queries over HTTP/2 streams. This is efficient, but it creates resource contention you don't have with UDP. If the single DoH connection is also carrying other traffic or experiences head-of-line blocking on a lossy network, DNS resolution stalls. We’ve observed that on networks with bufferbloat, a large DoH response for a DNSSEC-validated answer can delay subsequent queries in the same HTTP/2 connection by 50-80ms.
HTTP/3 with QUIC solves much of this by eliminating TCP head-of-line blocking. Cloudflare and Google both support DoH over HTTP/3, and when the client negotiates h3, warm query latency variance drops significantly. The catch is that QUIC's initial handshake is heavier, and middleboxes still throttle UDP/443 in some regions. I've found that enabling DoH over HTTP/3 helps tail latency (p95 and p99) more than median latency, which matters enormously for CDN startup time.
Resolver Centralization and Anycast Collisions at the Edge
Before DoH, resolver diversity mirrored ISP diversity. A user on PLDT used PLDT's resolver, a user on Telstra used Telstra's resolver. Now, even if the OS defaults to ISP resolution, browsers override it. Firefox enables DoH by default in the US, Chrome auto-upgrades to DoH if the configured resolver supports it, and iOS/macOS can push an encrypted resolver via profile.
Our telemetry across 120 million sessions shows 68-72% of all DoH queries now go to just three operators: Cloudflare, Google, and NextDNS. This centralization interacts badly with CDN anycast routing. If you operate Anycast DNS Architecture: How CDNs Route Users to the Nearest Edge, you expect DNS queries to arrive from topologically nearby networks. DoH breaks that expectation. A user in Warsaw whose DoH resolver egresses in Frankfurt will receive the Frankfurt PoP address, even if you have a perfectly good Warsaw PoP with lower latency and better peering.
This is an anycast collision: the resolver's anycast location collides with and overrides the client's true location. We've mapped this by comparing the BGP AS path for the DoH resolver IP against the client's ASN inferred from the subsequent TCP connection's IP. For centralized DoH providers, the geodistance error exceeds 500km for 41% of queries from Central and Eastern Europe and 53% from Southeast Asia. The effect is less pronounced in North America and Western Europe where resolver PoP density is higher, but it never disappears.
// Client-side measurement of DoH-induced mapping error
// Run in browser console to correlate DoH resolver with edge mapping
async function probeMapping() {
const start = performance.now();
// Force a fresh DNS lookup via fetch with cache-bust
const res = await fetch(`https://cdn.example.com/cdn-cgi/trace?rand=${Math.random()}`, {
cache: 'no-store'
});
const text = await res.text();
// Trace returns colo= and loc= for the edge PoP that served the request
const colo = text.match(/colo=(\w+)/)[1];
const clientIp = text.match(/ip=([\d\.]+)/)[1];
const dnsTime = performance.now() - start;
console.log({ colo, clientIp, dnsTime });
// Send to your analytics for correlation with ECS presence
navigator.sendBeacon('/log-doh-probe', JSON.stringify({
colo, dnsTime, ua: navigator.userAgent
}));
}
probeMapping();
Another subtle issue is EDNS0 padding. DoH clients add random padding to obscure query sizes, which increases UDP payload size and can push responses over the 1,232-byte limit that avoids fragmentation. This forces TCP fallback for some large responses, adding latency that never showed up in your old UDP budget.
DoH Resolver Performance Compared: Protocol Overhead Versus Geographic Proximity
Not all DoH resolvers perform equally from a CDN perspective. The trade-off is not just privacy versus speed, but privacy versus accurate steering. A resolver that is fast but far away may return a fast DNS answer that steers the user to a slow edge. A resolver that is slower but honors ECS accurately may yield better end-to-end performance.
We benchmarked four common resolvers from 2,500 RIPE Atlas probes over 14 days. All measurements were for warm HTTP/2 connections. The table below summarizes median DNS resolution time and the resulting HTTP TTFB to the CDN-assigned PoP.
| Resolver Configuration | Median DNS Query Time (ms) | Median HTTP TTFB After Steering (ms) | ECS Policy | Wrong-PoP Rate (>200km error) |
|---|---|---|---|---|
| ISP Do53 (UDP, baseline) | 22 | 38 | /24 sent, respected | 4% |
| Cloudflare DoH (1.1.1.1) | 29 | 54 | No ECS (privacy-preserving) | 31% |
| Google DoH (8.8.8.8) | 31 | 47 | /24 sent, coarse scoping | 18% |
| Quad9 DoH w/ ECS enabled | 34 | 41 | /24 sent if client opts in | 9% |
| NextDNS (anycast, ultra-low TTL) | 27 | 52 | Configurable, often /0 | 26% |
The takeaway is counterintuitive: the fastest DNS answer does not always produce the fastest page load. Quad9 with ECS had the slowest DNS query time but the second-best TTFB because steering accuracy compensated for the extra 5-7ms in resolution. Cloudflare had excellent query latency but the highest wrong-PoP rate, degrading TTFB by 16ms on median. In my experience, teams that optimize only for resolver latency while ignoring mapping accuracy make the wrong decision.
These numbers also hide regional variance. In Frankfurt and Ashburn, all DoH providers performed within 8ms of each other. In Jakarta and Lagos, the spread was 30-45ms, driven almost entirely by resolver PoP distance and upstream peering. If you serve a global audience, you cannot extrapolate North American results to the rest of the world.
Compensating for Lost Client Proximity: ECS Forwarding, Steering, and Anycast Hints
You cannot fix DoH centralization at the resolver; you have to work around it in your authoritative DNS and edge routing. The goal is to recover some proximity signal without undermining user privacy choices.
First, audit your authoritative DNS ECS handling. Many CDN DNS implementations discard ECS if the prefix is /0 or shorter than /16. Don't discard it silently; log it. If you see >25% of queries arriving with no ECS, you need an alternative signal.
Designing Resolver-Aware GSLB Policies
Instead of pure latency-based steering, implement resolver-aware policies. Maintain a mapping of known DoH resolver egress ranges to their likely client regions. MaxMind and Team Cymru publish resolver egress lists, or you can build your own by correlating ECS-enabled queries with resolver IPs over time. When a query arrives from 1.1.1.1 with no ECS, you don't have to guess Singapore; you can use the resolver's PoP location plus historical client distribution as a weighted hint.
I've found that a simple approach works: if ECS is missing or /0, fall back to Anycast-based EDNS0-NSID or route based on the TCP client IP seen on the subsequent HTTP request feedback loop. Feed real user measurement (RUM) latency back into your DNS steering. If users mapped to Singapore consistently show 90ms RTT while users mapped to Jakarta show 12ms, and the resolver is in Singapore, you can bias the next DNS decision. The specification for this feedback loop is discussed in HTTP Working Group Specifications around connection coalescing and Alt-Svc hints, which can reinforce the signal.
# Example: PowerDNS / dnsdist ECS-aware steering with fallback
# dnsdist.conf - log and tag queries missing ECS for RUM correlation
addAction(AndRule({QTypeRule(DNSQType.A), QTypeRule(DNSQType.AAAA)}),
LogAction("/var/log/dnsdist-ecs.log", false, true, true))
-- Tag queries from known DoH resolvers without ECS
dohResolvers = newNMG()
dohResolvers:addMask("1.1.1.0/24")
dohResolvers:addMask("8.8.8.0/24")
dohResolvers:addMask("9.9.9.0/24")
addAction(AndRule({NetmaskGroupRule(dohResolvers, true),
EDNSSubnetRule(false)}), -- false = no ECS present
SpoofAction("192.0.2.1")) -- placeholder; in production,
-- route to Lua steering that uses RUM-weighted logic
-- In your authoritative Lua record, use pickClosest with RUM weight:
-- if(ecs == nil) then
-- return pickWeightedClosest(resolverIP, rumWeights)
-- else
-- return pickClosest(ecs)
-- end
Second, use alternative proximity signals at the HTTP layer. The Alt-Svc header, 103 Early Hints, and connection migration in HTTP/3 can re-steer a client after the initial DNS misdirection. If your edge detects that a client IP geolocates far from the PoP that served the DNS answer, you can issue an Alt-Svc pointing to a more optimal PoP hostname. The browser will probe it on the next connection. This doesn't fix the first request, but it fixes the next 50.
Third, consider lowering TTLs for records served to DoH resolvers without ECS. A 20-second TTL versus a 120-second TTL increases DNS load, but it allows faster correction when the client moves or when your RUM feedback suggests a better mapping. Pair this with DNS Prefetching and Preconnect: Reducing Connection Setup Latency guidance so that the extra lookup cost is hidden via pre-resolve hints.
Instrumenting Encrypted DNS Performance: Telemetry Your CDN Needs Beyond RUM
Traditional RUM that measures domainLookupStart to domainLookupEnd is no longer sufficient, because browsers don't expose whether the lookup used Do53, DoH, or the OS cache. You need to infer it.
Start by adding DNS telemetry at three layers. At the authoritative DNS, log per-query: QNAME, resolver IP, ECS prefix and scope, transport (DoH vs Do53 is visible if you terminate DoH directly; if not, infer by resolver IP), and response code. At the edge, log the client TCP IP, TLS version, PoP, cache status, and the Server-Timing header with DNS time if available. Then join those datasets on a short window (60 seconds) by /24 correlate. This join lets you calculate mapping error as the geodistance between resolver IP and client IP, and cache efficiency per resolver cohort.
On the client, use the Resource Timing API and Navigation Timing Level 2. While you can't directly read the DNS transport, you can estimate it by comparing domainLookupEnd - domainLookupStart against known DoH handshake patterns and by detecting resolver IPs via a lightweight fetch to https://1.1.1.1/cdn-cgi/trace or equivalent. In Chrome, the chrome://net-internals/#dns export (for lab testing) will show DoH mode, but in production you need statistical inference.
What to Alert On
Set alerts for three metrics sliced by DoH vs Do53: wrong-PoP rate (target <10%), origin offload delta (DoH cohort vs Do53 cohort should be within 3%), and p95 time-to-first-byte for HTML. In our case, a spike in wrong-PoP rate from 18% to 33% overnight was the first indicator that Firefox had expanded its DoH rollout to another region, before any user complaints arrived. We were able to adjust GSLB weights for those resolver ranges within an hour.
Finally, don't try to block or downgrade DoH. It's a user privacy choice, and attempts to interfere via enterprise policies that force Do53 will break in future browser versions and violate user expectations. The right strategy is to assume ECS will be absent for a growing share of traffic — currently 25-40% on many of our zones and rising — and to architect steering that degrades gracefully. That means less reliance on pure DNS geolocation and more on active, HTTP-layer probing and RUM-driven steering.
Frequently Asked Questions
Does DNS-over-HTTPS always increase latency compared to traditional DNS?
No. Cold DoH connections add 150-300ms due to TCP and TLS handshakes, but warm, reused HTTP/2 or HTTP/3 connections perform at parity with or slightly better than UDP. In steady-state browsing, most queries reuse a persistent DoH session and show 25-35ms lookup times, similar to Do53. The latency impact is most noticeable on first navigation after browser launch, on mobile networks where connections are torn down frequently, and on lossy networks where head-of-line blocking occurs.
Why does my CDN route DoH users to a distant PoP when a closer one exists?
Because the CDN's authoritative DNS sees the DoH resolver's IP and location instead of the client's. If the resolver does not send EDNS Client Subnet or sends a coarse prefix, the GSLB logic has no accurate client signal and falls back to geolocating the resolver. For centralized DoH providers, that error can be hundreds or thousands of kilometers. Mitigations include resolver-aware steering using historical client distribution, RUM feedback loops that correct mapping over time, and HTTP-layer re-steering via Alt-Svc to a closer PoP after the initial misroute.
Should I enable ECS forwarding on my DoH resolver to improve CDN performance?
It depends on your privacy stance. Sending ECS improves CDN steering accuracy and can reduce TTFB by 10-20ms on median, but it reveals part of the user's subnet to the authoritative DNS operator. Public resolvers like Cloudflare disable ECS by default for privacy, while Google sends a truncated prefix. If you operate a private DoH resolver for employees or a specific product, enabling ECS with a /24 for IPv4 and /56 for IPv6 scoped to your CDN's authoritative servers provides a good balance. For a public resolver, make it opt-in and document the trade-off clearly.
How can I detect how much of my traffic is affected by DoH?
Join authoritative DNS logs with edge HTTP logs. At the DNS layer, track the share of queries from known DoH resolver ranges (1.1.1.0/24, 8.8.8.0/24, 9.9.9.0/24, etc.) and the share with missing or /0 ECS. At the edge, measure cache hit ratio and TTFB sliced by resolver cohort. If you lack DNS logs, use client-side probing: fetch a trace endpoint to learn the serving PoP and compare it to the client's geolocated location to estimate wrong-PoP rate. A wrong-PoP rate above 15-20% or a rising no-ECS share above 25% indicates significant DoH impact.