A few years ago we onboarded a live sports streaming customer who kept getting complaints from viewers in Chicago — their streams were buffering while users in New York were fine. The CDN edge map looked perfect on paper, but our geo-routing was sending half the Midwest to the Virginia PoP instead of the much faster Chicago edge. Switching that one hostname to HTTP Working Group Specifications compliant health checks and latency-based routing cut their rebuffering rate by 41% overnight. That’s when I learned geography doesn’t equal performance.
How Latency-Based Routing Decides What "Fastest" Really Means
When most folks first hear about latency-based routing, they assume it’s just a fancier version of geo-routing. It isn’t. Geographic routing looks at where a resolver claims to be and sends you to the nearest physical PoP. DNS performance routing, on the other hand, doesn’t care where you are on a map — it cares how long it actually takes a packet to get back and forth. I’ve seen cases where a user in Dallas gets better performance from Seattle than from the Dallas edge because of peering congestion or a saturated transit link that night. Latency DNS exists to catch those real-world quirks.
Under the hood, the authoritative DNS system keeps a continuously updated table of round-trip times from different resolver networks to each of your edge locations. Providers like AWS, Cloudflare, and NS1 do this by sampling TCP and HTTP handshake times from their own backbone, plus active probes from resolvers that have opted into measurement. When a query comes in from, say, 8.8.8.8 in Iowa, the system checks which of your edges has historically responded fastest to that resolver’s /24 or to that region, and returns that IP. You’re not directing users to the closest edge, you’re directing them to the fastest edge for that network at that moment.
That distinction matters more than you’d think once you’ve dealt with anycast quirks or uneven backbone peering. I remember debugging a latency spike for an e-commerce site during Black Friday — their latency-based routing was working, but we’d left a static TTL of 300 seconds on a failing edge while health checks were disabled for that record. Users kept getting stuck on a degraded PoP for five minutes at a time. It’s a small configuration detail that turns a smart routing policy into a foot-gun if you don’t pair it with aggressive health checks and short TTLs. If you’re still pointing your apex domain with a traditional CNAME hack, you’ll want to sort that first — see our breakdown of DNS CNAME Flattening: Pointing Root Domains to CDN Without Breaking DNS Rules before you layer latency logic on top.
# Example: Terraform for AWS Route 53 latency-based routing
# Two identical records, different regions and latency targets
resource "aws_route53_record" "cdn_edge_us_east" {
zone_id = aws_route53_zone.main.zone_id
name = "cdn.example.com"
type = "A"
ttl = 60 # keep it low for latency DNS
latency_routing_policy {
region = "us-east-1"
}
set_identifier = "us-east-1-edge"
records = ["192.0.2.10"] # Virginia edge VIP
health_check_id = aws_route53_health_check.edge_va.id
}
resource "aws_route53_record" "cdn_edge_us_west" {
zone_id = aws_route53_zone.main.zone_id
name = "cdn.example.com"
type = "A"
ttl = 60
latency_routing_policy {
region = "us-west-2"
}
set_identifier = "us-west-2-edge"
records = ["192.0.2.20"] # Oregon edge VIP
health_check_id = aws_route53_health_check.edge_or.id
}
# Verify which edge a resolver sees:
# dig cdn.example.com @8.8.8.8 +ednsopt
Without Correct Client Data, Even the Best Latency Table Is Guessing
Here’s the dirty secret of DNS: the server you talk to usually isn’t the user. When someone in Berlin queries your authoritative DNS, you often just see the IP of their ISP’s resolver in Frankfurt, or worse, a centralized Google or Cloudflare resolver hundreds of miles away. If your latency-based routing only sees the resolver’s location, your fastest edge calculation is based on the wrong starting point. That’s why I don’t deploy latency DNS without verifying what client information we’ll actually receive.
That’s where EDNS Client Subnet (ECS) comes in. ECS lets recursive resolvers forward a truncated slice of the client’s subnet (typically a /24 for IPv4) with the query, so your authoritative server can make a decision for the user’s network, not the resolver’s. Without it, you’re essentially optimizing for the path between your edge and 8.8.8.8, not the viewer on their couch. The performance difference is stark on mobile carriers and enterprise networks where DNS is centralized far from the user. I’ve audited setups where enabling ECS shaved 30-45ms off p95 response times simply because we stopped sending rural users to the wrong coast. For a deeper look at how this works, check EDNS Client Subnet for CDN Geolocation: Routing Users to the Nearest Edge.
You’ll also want to think about how you delegate authority for that routing intelligence. A lot of teams try to manage complex routing policies inside their domain registrar’s basic DNS, which lacks the sampling and health-check infrastructure. Offloading the CDN hostname to dedicated DNS infrastructure via NS records gives your latency engine room to do its job without touching the rest of your zone. We cover that pattern in detail in DNS NS Delegation for CDN: Offloading Domain Resolution to Edge Networks. And when you’re validating performance impact, don’t just stare at DNS lookup time — correlate it with real user-centric metrics as defined by web.dev Articles on LCP and TTFB, since a faster DNS answer that points to a congested edge isn’t a win.
#!/usr/bin/env python3
# Simple script to compare latency-based vs geo DNS answers
# Requires dnspython: pip install dnspython
import dns.resolver
import dns.edns
import time
import statistics
HOST = "cdn.example.com"
RESOLVERS = ["8.8.8.8", "1.1.1.1", "9.9.9.9"]
ECS_PREFIX = "203.0.113.0/24" # Simulate client in Chicago
def query_with_ecs(resolver_ip, ecs=None):
resolver = dns.resolver.Resolver(configure=False)
resolver.nameservers = [resolver_ip]
# Add ECS option to simulate client subnet
ecs_option = dns.edns.ESubnetOption.from_text(ecs) if ecs else None
start = time.time()
try:
answer = resolver.resolve(HOST, "A", raise_on_no_answer=False)
elapsed = (time.time() - start) * 1000
ips = [str(r) for r in answer] if answer.rrset else []
return ips, elapsed
except Exception as e:
return [f"error: {e}"], 0
for ns in RESOLVERS:
ips_no_ecs, t1 = query_with_ecs(ns)
ips_ecs, t2 = query_with_ecs(ns, ECS_PREFIX)
print(f"Resolver {ns}:")
print(f" Without ECS -> {ips_no_ecs} in {t1:.1f}ms")
print(f" With ECS {ECS_PREFIX} -> {ips_ecs} in {t2:.1f}ms")
When Low-Latency Routing Hurts: Flapping, Cold Caches, and Cost
Latency-based routing isn’t a set-and-forget optimization. The first time I enabled it for a news site, we celebrated a 25% drop in median TTFB — then watched our origin bill spike and cache hit ratio tank. Why? The fastest edge kept shifting between New York, Philadelphia, and Ashburn based on tiny latency jitters (5-10ms), so users bouncing between edges never warmed a cache long enough to benefit. We’d built DNS performance routing that was too sensitive. You’ve got to dampen flapping by tuning health check intervals and using sticky latency buckets rather than reacting to every millisecond swing.
This is where you have to decide what latency DNS should and shouldn’t do for your architecture. It excels at steering new connections to the best-performing PoP, but it can’t fix a poorly distributed cache or an edge that’s fast to ping yet slow to deliver because its disk is thrashing. If your edges have vastly different cache populations — common when you’ve just added a new PoP — the fastest edge on paper may have a 40% miss rate while a slightly slower edge would have served the object from cache. That’s why we always pair latency routing with layer-two consistent hashing or an origin shield, and monitor both network latency and application latency together.
The table below shows anonymized numbers from a recent A/B test I ran for a SaaS customer with four U.S. edges. Same content, same origin, only the DNS routing policy changed over a 7-day period. The results illustrate why latency-based routing wins on raw speed but needs supporting architecture:
| Routing Policy | Median DNS Lookup (ms) | Median TTFB (ms) | Edge Cache Hit Ratio | Origin Egress Cost (Relative) |
|---|---|---|---|---|
| Geographic (GeoDNS) | 38 | 142 | 89% | 1.00x (baseline) |
| Latency-Based (60s TTL) | 42 | 98 | 76% | 1.31x |
| Latency-Based + Shield (60s TTL) | 43 | 105 | 91% | 1.04x |
| Anycast (No DNS steering) | 31 | 118 | 88% | 1.02x |
Putting Latency-Based Routing Into Production: From Table to Traffic
In theory, latency-based routing is elegant — you’ve got a matrix of client prefixes to PoPs with measured RTTs, and DNS just hands back the winner. In production, you’ll learn it’s never that clean. The first thing we’ve seen trip up teams is assuming their authoritative DNS actually sees the client. If your provider doesn’t support EDNS Client Subnet (ECS) or your fallback logic is weak, you’re making decisions based on the resolver’s location, not the user’s. That’s why you’ll want an authoritative platform that ingests ECS — Cloudflare, Route 53, NS1, and PowerDNS with the right recursor all do — and you must actively measure from the resolver *and* the ECS-derived client subnet.
The second production reality is TTL and health checking. Set your TTL too high (say, 300s+) and you can’t react when a PoP’s latency spikes from 18ms to 140ms after a fiber cut in Marseille. Set it too low (under 10s) and you’ll hammer your authoritative infrastructure and increase p95 DNS resolution time by 30-45ms due to cache misses. For most global CDNs, we’ve landed on 30-60 seconds as the sweet spot for latency records, paired with aggressive health checks every 10-15 seconds that actually fetch an object, not just ICMP pings. A PoP that responds to ping but can’t serve cache isn’t healthy — your probe needs to validate HTTP 200s from the edge.
# Terraform: Latency-based records with health checks and ECS-aware routing
# This is a simplified Route 53 example - 60s TTL for agility without cache thrash
resource "aws_route53_health_check" "iad_edge" {
type = "HTTPS"
resource_path = "/healthcheck/__edge_probe"
failure_threshold = 3
request_interval = 10
measure_latency = true
regions = ["us-east-1", "eu-west-1", "ap-northeast-1"]
}
resource "aws_route53_record" "www_iad" {
zone_id = aws_route53_zone.main.zone_id
name = "www.example.com"
type = "A"
ttl = 60
set_identifier = "iad-latency"
latency_routing_policy {
region = "us-east-1"
}
health_check_id = aws_route53_health_check.iad_edge.id
records = ["192.0.2.10"]
}
resource "aws_route53_record" "www_fra" {
zone_id = aws_route53_zone.main.zone_id
name = "www.example.com"
type = "A"
ttl = 60
set_identifier = "fra-latency"
latency_routing_policy {
region = "eu-west-1"
}
health_check_id = aws_route53_health_check.fra_edge.id
records = ["198.51.100.20"]
}
Advanced Patterns and Troubleshooting When "Fastest" Isn't Best
Once you’ve got basic latency routing running, you’ll hit the edge cases that don’t show up in vendor docs. The biggest one is latency flapping. When two PoPs are within 8-12ms of each other for a given client prefix — think users in central Poland choosing between Warsaw and Frankfurt — small jitter can cause your routing table to flip back and forth every measurement window. That thrashing is brutal for cache hit ratio. We’ve seen hit rates drop from 96% to 71% when users bounce between PoPs every 60 seconds, because each edge has to do a cold fetch to origin. The fix isn’t to chase the absolute lowest RTT. Implement hysteresis: don’t switch PoPs unless the new winner is at least 15-20% or 10ms better for 3 consecutive measurement intervals. It’s less “optimal” on paper, but your origin shield and TTFB will thank you.
Another pattern we rely on heavily is the hybrid Anycast + DNS latency model. Pure DNS latency routing gives you surgical control, but it can’t fail over as fast as BGP when a whole region goes dark. We advertise the same IP via Anycast in 4-5 super-PoPs and use DNS latency routing to steer users *within* a region to the unicast edge that’s actually fastest. If DNS fails or TTLs haven’t expired, Anycast still gets the packet somewhere close. For observability, don’t trust your synthetic probes alone. Correlate your latency table against real user monitoring (RUM) data — if your probes say FRA is 22ms for AS15169 in Berlin but RUM shows 68ms p95, you’ve got a peering or congestion issue your probe network isn’t seeing. And while you’re hardening this, remember DNS is still DNS: you’ll want to review your DNSSEC Signing for CDN Infrastructure: Authenticated DNS Without the Performance Hit strategy so latency records can’t be spoofed, and ensure your mitigation for DNS Amplification Attack Mitigation: Protecting CDN DNS Infrastructure doesn’t accidentally rate-limit legitimate ECS queries.
If you see sudden latency-driven misrouting, check ECS privacy filtering first. Many public resolvers truncate ECS to /24 for IPv4 or /56 for IPv6, or strip it entirely for privacy. When that happens, you’re back to resolver-based routing. We keep a secondary GeoIP fallback table specifically for /0 or missing ECS cases — it’s not as precise, but it prevents a user in Sydney from being sent to Seattle just because they use a US-based resolver. For malicious or anomalous client subnets that keep probing your latency probes, we isolate them via DNS Sinkholing at CDN Scale: Redirecting Malicious Traffic for Analysis rather than letting them poison your RTT measurements.
Frequently Asked Questions
Does latency-based routing require EDNS Client Subnet to be effective?
It doesn’t strictly require it, but without ECS you’re routing based on the resolver’s location, not the end user’s. In our measurements, that creates a 20-40% misrouting rate for ISPs that use centralized resolvers like Google Public DNS or Cloudflare 1.1.1.1. If your DNS provider supports ECS, enable it and make sure your latency table is built from both resolver IPs and ECS subnets. Always keep a GeoIP fallback for queries where ECS is stripped or truncated.
What TTL should I use for latency-based DNS records?
We recommend 30 to 60 seconds for most CDN workloads. Below 20 seconds, you’ll see a sharp increase in authoritative query volume — often 3-5x — and higher DNS resolution latency for users due to cache misses. Above 120 seconds, you lose agility to react to PoP congestion or failure, and clients will stick to a degraded PoP far too long. Pair your TTL with 10-second health checks that validate at the HTTP layer, not just network reachability.
Why are users flapping between two PoPs and hurting my cache hit ratio?
That’s classic hysteresis failure. When two PoPs have nearly identical RTTs (within ~10ms), normal network jitter will make the “winner” alternate every minute or two. Each flap sends the user to a cold cache, forcing an origin fetch and adding 150-400ms to TTFB. Add a damping rule: only switch the preferred PoP if it’s consistently 10ms or 15% faster for at least 2-3 consecutive measurement cycles (about 3-5 minutes). You’ll sacrifice a few milliseconds of theoretical latency for a 15-25 point improvement in cache hit ratio.
Should I use DNS latency routing, Anycast, or both?
For a global CDN, use both. Anycast gives you instant, BGP-driven failover and minimizes DNS lookup latency since one IP is announced everywhere — ideal for surviving regional outages in under 30 seconds. DNS latency routing gives you finer, policy-aware control to steer traffic based on measured performance, cost, or capacity. Our standard pattern is Anycast for super-PoP ingress and DNS latency routing to select the specific edge within the region. It’s more complex to operate, but it’s the only way we’ve consistently delivered sub-50ms p95 TTFB globally without sacrificing resilience.