Back in 2018, I watched a customer in Cape Town get routed to our London edge — 9,200 km away — when we had a perfectly healthy PoP in Johannesburg 30 km from him. His ISP's recursive resolver was in London, and without any clue about where the actual user was, our authoritative DNS did what it thought was best: sent him to London. That single misroute added 240ms to every request, and it wasn't an outlier. It was happening millions of times a day before we turned on EDNS Client Subnet.
Why Resolver Location Lies About User Location
For years, most DNS NS Delegation for CDN: Offloading Domain Resolution to Edge Networks setups made a flawed assumption: the resolver's IP is close to the user's IP. With anycast resolvers like 8.8.8.8, 1.1.1.1, and large ISP central resolvers, that assumption breaks completely. I've seen traces where a user in São Paulo hits a resolver anycast node in Miami, a user in Sydney gets answered by a resolver in Singapore, and our DNS CDN routing logic — looking only at the resolver's /24 — picks the wrong continent. We were optimizing for the wrong IP.
Standard DNS has no field for "here's where the client actually is." The query you receive at your authoritative server contains only the resolver's source address. You can try to patch it with anycast for your own authoritative tier, or with DNS CNAME Flattening: Pointing Root Domains to CDN Without Breaking DNS Rules to keep the apex performant, but neither tells you where the eyeball lives. We tried GeoIP on resolver IPs for about six months in 2016 and gave up. The P95 latency got worse, not better, because we were confidently wrong. The fix isn't better GeoIP databases — it's getting the client's subnet in the query itself.
That's what EDNS Client Subnet (ECS) — defined in RFC 7871 — actually does. It's an EDNS0 option (code 8) where the resolver forwards a truncated version of the client's IP prefix, like 203.0.113.0/24, not the full address. Your authoritative server can then make a geolocation routing decision on that prefix instead of the resolver's location. It isn't magic, and it doesn't give you a GPS coordinate; it gives you a network block you can map to a region, and that's usually enough to pick the right metro.
# Without ECS: Autoritative server only sees resolver IP (e.g., 8.8.8.8)
$ dig @ns1.cdn-example.net www.example.com A
; Query from 8.8.8.8 (Mountain View, US) for user in Cape Town, ZA
www.example.com. 30 IN A 151.101.12.34 ; LHR - London (wrong!)
# With ECS: Resolver forwards truncated client subnet
$ dig @8.8.8.8 www.example.com A +subnet=196.212.0.0/20 +subnet=196.212.0.0/24
; EDNS: version: 0, flags:; udp: 1232
; CLIENT-SUBNET: 196.212.0.0/20/0
; -> Authoritative sees 196.212.0.0/20 (Cape Town, ZA)
www.example.com. 30 IN A 192.0.2.10 ; JNB - Johannesburg (correct)
www.example.com. 30 IN A 192.0.2.11
; SCOPE: 196.212.0.0/20 - response cached per /20, not globally
How ECS Works on the Wire Without Breaking Privacy or Caching
The mechanics are deceptively simple but easy to get wrong in production. When a supporting resolver — Google Public DNS, Cloudflare, OpenDNS all do now — gets a query from 196.212.45.78, it doesn't forward 196.212.45.78. It masks it to a prefix length it considers safe, typically /24 for IPv4 and /56 for IPv6, and stuffs that into the OPT record as OPTION-CLIENT-SUBNET. Your authoritative server reads it, does a longest-prefix match against your geolocation map, and returns an answer with a SCOPE prefix that says "this answer is valid for everyone in 196.212.0.0/20." The resolver then caches that answer scoped to that prefix, not globally. Details on EDNS(0) framing are covered in the HTTP Working Group Specifications and related DNS extension drafts.
That scoping is where most teams trip. If you respond with SCOPE /24 but your mapping is actually /20 accurate, you've just shattered your cache for no reason — you'll store 16x more entries and get hammered with fills. I made this mistake on our first rollout: we echoed back /24 for everything, cache hit ratio on our resolver partners dropped from 92% to 71% overnight, and query volume to our authoritative tier spiked 4x. We learned to return the least-specific prefix that still yields the correct PoP. For dense metros like the US East Coast you might need /24 granularity; for most of Africa or South America, a /20 or even /16 gives you the same PoP selection with far less cache fragmentation. You also have to handle the privacy case: if the resolver sends 0/0 or doesn't send ECS at all, you can't just fail — you need a sane fallback, usually resolver IP geolocation or anycast steering.
# Knot DNS / PowerDNS style ECS handling - return minimal scope
# ecs.conf - authoritative config
ecs-ipv4-bits = 24 # what we accept from resolvers
ecs-ipv6-bits = 56
ecs-cache-limit = 150000
-- Lua policy for geolocation routing (PowerDNS)
function preresolve(dq)
-- dq.ecs is like "196.212.0.0/20" or nil if no ECS
local ecs = dq.ecs or dq.remoteaddr:toString()
local region = geoip.lookup(ecs) -- MaxMind / internal DB
if region == "ZA" or region == "MZ" then
dq:addAnswer(pdns.A("192.0.2.10")) -- JNB
dq.scopeMask = 20 -- cache this for entire /20, not /24
return true
elseif region == "BR-SOUTH" then
dq:addAnswer(pdns.A("192.0.2.20")) -- GRU
dq.scopeMask = 20
return true
end
-- fallback: no ECS or unknown region
dq:addAnswer(pdns.A("151.101.12.34")) -- LHR default
dq.scopeMask = 0
return true
end
RTT Wins vs. Cache Fragmentation: What We Measured in Production
When we finally tuned scope masks correctly, the results were immediate. We A/B tested ECS-aware routing against resolver-IP routing on about 15% of global traffic for two weeks — same PoPs, same anycast, same DNSSEC Signing for CDN Infrastructure: Authenticated DNS Without the Performance Hit chain, only the routing input changed. For users on centralized or anycast resolvers (about 38% of our traffic at the time), median TTFB dropped 35-60ms. For users on ISP resolvers that were already local, there was almost no difference, which is exactly what you'd expect. Don't expect ECS to help if your users all run local ISP resolvers next door — it helps when the resolver topology doesn't match the eyeball topology.
The trade-off is cache efficiency, and you can't ignore it. Every distinct SCOPE you return is a distinct cache entry at the resolver. If you get greedy and return /32s, you'll drive cache hit ratios into the ground and push more queries to your authoritative infrastructure, which increases tail latency and can hurt resilience. Guidance from web.dev Articles on DNS and connection setup performance lines up with what we saw: shorter DNS TTLs and more cache misses don't just add DNS latency, they delay TCP and TLS handshakes too. The sweet spot we'd found after testing /16 through /32 across regions was /20 to /24 for IPv4. It gave us 90%+ of the geolocation accuracy with a manageable cache hit penalty.
| ECS Configuration | P50 TTFB (Anycast Resolver Users) | Resolver Cache Hit Ratio | Auth QPS (per million users) |
|---|---|---|---|
| No ECS (resolver IP only) | 182 ms | 93.4% | 1,100 qps |
| ECS /24 scope (max precision) | 118 ms | 71.2% | 4,250 qps |
| ECS /20 scope (balanced) | 122 ms | 88.7% | 1,650 qps |
| ECS /16 scope (coarse) | 145 ms | 91.9% | 1,280 qps |
| ECS /24 with ECS-aware TTL 60s | 121 ms | 84.5% | 1,920 qps |
Putting ECS Into Production Without Tanking Your Cache
We learned the hard way that you don't just flip enable-ecs yes and call it a day. The first time we rolled it out on our authoritative cluster, our cache hit ratio dropped from 94% to 71% overnight. The problem wasn't ECS itself — it was how we keyed the cache. If you key on the full /24 you get from Google Public DNS or Cloudflare's 1.1.1.1, you'll end up with millions of tiny cache shards. What worked for us was aggregating at the edge: we accept a /24 or /56 from the resolver, but we map it to a covering /20 (IPv4) or /48 (IPv6) for cache lookup. That alone brought our hit ratio back to 91.5% while still giving us metro-level accuracy. We've found /24 for IPv4 and /56 for IPv6 is the sweet spot — precise enough to route Jakarta users away from Singapore, coarse enough that you're not leaking anything personally identifiable.
On the authoritative side, you need to actually *do* something with the subnet. Most modern stacks — PowerDNS Authoritative 4.7+, BIND 9.18+ with GeoIP2, Knot DNS — will parse the ECS option automatically if you tell them to. The trick is your fallback logic. About 18-22% of queries we see still come in with ECS disabled or with a scope of 0/0, especially from smaller ISP resolvers and corporate forwarders. If you don't handle that case, those users get routed to your default PoP, which might be 90ms away. Our logic is simple: if ECS is present and valid, use it; if it's missing or 0, fall back to the resolver's IP geolocation; if both fail, return our Anycast-optimized answer. Don't forget to set the SCOPE prefix-length in your response correctly — if you answer based on a /20, tell the resolver you did, so it can cache appropriately for the next /24 inside that /20.
Testing before you go global is non-negotiable. We run a canary nameserver that logs every ECS query's subnet, the PoP we selected, and the scope we returned. That let us catch a bug where we were returning a scope of /24 but making decisions on a /20, which caused resolvers to cache too narrowly and hammer us with 3.4x more queries. Use dig +subnet to spoof everything locally before you touch production.
# Test and verify ECS handling on your authoritative
# Query as if you're a user in 203.0.113.0/24 (e.g., Sydney)
dig @ns1.yourcdn.net www.example.com +subnet=203.0.113.0/24 +short
# PowerDNS Authoritative - dnsdist front-end with ECS pooling
-- dnsdist.conf: aggregate ECS to /20 for caching, keep /24 for routing
pc = newPacketCache(1000000, {maxTTL=86400, minTTL=1, temporaryFailureTTL=60, staleTTL=60, dontAge=false})
getPool("authoritative"):setCache(pc)
addAction(EDNSOptionRule(EDNSOptionCode.ECS), PoolAction("authoritative"))
-- In your backend, map /24 -> /20 for cache key but log original /24
# BIND 9.18+ - enable ECS processing with GeoIP2
options {
edns-udp-size 1232;
geoip-directory "/usr/share/GeoIP";
ecs-cache yes;
};
Advanced Patterns and Troubleshooting ECS at Scale
Once you've got the basics working, you'll hit the weird edge cases. The biggest one is Anycast + ECS interaction. If you're already anycasting your authoritative NS, ECS can actually hurt you if your anycast node selection is poor. We saw cases where a user in Milan queried a resolver in Frankfurt, which sent ECS 185.12.45.0/24, but the query hit our anycast NS in Amsterdam due to BGP. Our Amsterdam node had a cold cache for that Italian subnet and returned a suboptimal PoP. Fixing it meant deploying ECS-aware cache sharing between our NS nodes via a 12ms Redis replication mesh, and more importantly, grouping subnets by metro, not by raw prefix. Now a /20 covering Milan maps to the same cache entry regardless of which NS node answers.
Cache fragmentation is still the #1 reason teams roll back ECS, so watch your metrics like a hawk. After enabling ECS, we expected a 15-20% increase in cache entries — we got 280% until we implemented aggregation. Monitor CacheEntries, QPS per NS node, and Origin Shield offload. If your entries double week-over-week, you're slicing too fine. Another pattern that saved us: ECS scoping with EDNS0 padding and response ECS stripping for privacy-sensitive regions. For EU traffic, we honor the resolver's source prefix-length strictly and never echo back a more specific scope than we were given. And if you're seeing inconsistent routing, check whether the public resolver is even forwarding ECS — Cloudflare 1.1.1.1 famously doesn't send ECS by default for privacy, while Google's 8.8.8.8 sends /24 and /56. You'll need both paths tested.
Don't treat ECS as just a performance feature — it's a control plane. Once you can see the actual client subnet, you can do far more intelligent traffic steering. We use subnet visibility to feed our DNS Sinkholing at CDN Scale: Redirecting Malicious Traffic for Analysis pipeline, so botnet C2 lookups from infected /24s get sinkholed to a scrubbing PoP instead of the nearest edge. It also changes how you defend your DNS itself; with ECS, your authoritative QPS goes up 2-3x, which expands your surface for DNS Amplification Attack Mitigation: Protecting CDN DNS Infrastructure — rate limiting has to be subnet-aware, not just IP-aware. And finally, tie it into your health checks. Our ECS logic is integrated with DNS Failover Automation for CDN: Automatic Rerouting When Origins Go Down, so if Milan goes unhealthy, we automatically widen the scope for Italian subnets to route to the next-best PoP in Zurich without waiting for cache expiry. Without that link, ECS just routes users faster to a dead edge.
Frequently Asked Questions
Does ECS expose my users' full IP addresses to authoritative servers?
No, not if it's implemented correctly. Reputable public resolvers truncate the address before sending it — Google truncates IPv4 to a /24 and IPv6 to a /56, and many others do /24 or even /20. That leaves 254+ addresses in the subnet, so you get city or metro-level geolocation without identifying an individual household. You should never configure your authoritative to request or store a longer prefix than /24 for IPv4. We also strip and discard the ECS option after routing, we don't log the raw subnet beyond aggregated /20 stats.
Why did our cache hit ratio collapse after enabling ECS?
Because you're probably caching per /24 instead of per aggregated region. Each unique ECS subnet creates a separate cache entry for the same name. With 3 million active /24s on the internet, that's cache death. The fix is subnet aggregation for the cache key — map incoming /24s to their covering /20 or /19 for lookup, but still make the routing decision on the original /24. Also ensure you're setting the SCOPE prefix-length correctly in the response. If you decide based on a /20 but return scope /24, resolvers will re-query for every /24 and your QPS will spike 2-4x.
Should we use ECS with Anycast, or is it redundant?
They're complementary, not redundant. Anycast gets the user to the nearest *nameserver*, ECS gets the user to the nearest *edge cache*. Without ECS, Anycast alone routes based on resolver location — our data showed 31% of users in Southeast Asia were being sent to US West because their resolver was anycasted there. With ECS, we cut median RTT from 87ms to 54ms for those users. Use Anycast for DNS resilience and low DNS lookup latency, use ECS for accurate asset routing.
What happens when a resolver doesn't support ECS?
You fall back gracefully. About one in five queries still arrives without ECS, and OpenDNS, Cloudflare, and some ISP resolvers disable it by default. Your authoritative should detect the missing option and fall back to geolocating the resolver's IP itself — which is still better than SERVFAIL. We return a response with scope 0, meaning "this answer is good for anyone who asked this nameserver." Don't drop or refuse queries without ECS; you'll just blackhole legitimate users.