DNS Debugging: Who Answered, and With What?
A name lookup can be answered by half a dozen different caches and resolvers before it reaches anyone authoritative, so the first question in every DNS problem is "which of them answered?" — and dig against a chosen server, +trace and the TTL in the answer will tell you.
The problem
api.example.com to point at the new load balancer an hour ago. Half the users see the new one, the other half the old one, your laptop sees a third answer, and curl and the browser disagree. Every one of them is telling the truth about what it was told.Where the answer could have come from
When code asks for api.example.com, the answer comes from the first place in a chain that has one. The browser has its own cache (Chrome: chrome://net-internals/#dns). The OS has a hosts file, which wins over everything. The OS has a resolver cache — systemd-resolved on many Linux distributions, mDNSResponder on macOS, the DNS Client service on Windows. Then the configured recursive resolver (your router, the ISP, 8.8.8.8, the corporate one pushed by VPN) has a cache. Only after all of these miss does anyone walk root → TLD → authoritative. Each link caches for the record’s TTL, independently.
This is why "DNS propagation" is a misnomer. Nothing propagates. Each cache serves the old answer until its own copy of the TTL runs out, and caches that were populated at different moments expire at different moments. A change to a record with a 3600 s TTL is fully visible only after every cache that held the old value has aged it out — up to an hour after the change, and longer for resolvers that clamp minimum TTLs.
- Linux:
resolvectl statusshows which upstream each interface uses;resolvectl query nameandresolvectl flush-caches./etc/resolv.confpointing at127.0.0.53means the stub resolver is in the path. - macOS:
scutil --dnslists resolvers per scope;dscacheutil -q host -a name api.example.comqueries the way apps do; flush withsudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder. - Windows:
ipconfig /displaydns,ipconfig /flushdns,Resolve-DnsName api.example.com -Server 8.8.8.8,Get-DnsClientServerAddress.
- Browser cacheper-browser, seconds to a minute; Chrome and Firefox may bypass the OS entirely via DoH↓
- Hosts file`/etc/hosts` · `C:\Windows\System32\drivers\etc\hosts` — static, wins over DNS, the source of "works on my machine"↓
- OS resolver cachesystemd-resolved / mDNSResponder / DNS Client; honours TTL↓
- Configured recursive resolver`/etc/resolv.conf`, DHCP, VPN-pushed; has its own cache and possibly its own private zones↓
- Root → TLD → authoritativeonly reached on a full miss; `dig +trace` reproduces this walk
Reading dig
Four things in a dig reply matter. The status: NOERROR (an answer or an empty-but-valid one), NXDOMAIN (the name does not exist — the whole name, at the authoritative level or cached as such), SERVFAIL (the resolver failed to get an answer: broken delegation, DNSSEC failure, unreachable authoritative servers), REFUSED (the server will not answer for you — usually asking an authoritative server recursively). The flags: aa means the answer is authoritative, its absence means it came from a cache. The answer section: the record chain, often CNAME → CNAME → A, each with its own TTL. And the SERVER line at the bottom, which says who you actually asked.
The TTL column is a clock. An authoritative answer shows the configured TTL (300, 3600). A cached answer shows the remaining TTL counting down; run dig twice and watch it decrease. A TTL that resets to the full value on every query means you are hitting the authoritative server or a cache that just refreshed; a TTL of 3412 tells you the cache fetched it 188 s ago and will hold the old value for 57 more minutes.
An empty NOERROR answer (status NOERROR, ANSWER: 0) is the "NODATA" case: the name exists but has no record of the type you asked for — typically AAAA for an IPv4-only host, or asking A for a name that only has a CNAME whose target failed. See DNS Record Types and What They Are For.
$ dig api.example.com ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 41922 ;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 ;; ANSWER SECTION: api.example.com. 212 IN CNAME lb-prod-eu.example.net. lb-prod-eu.example.net. 47 IN A 203.0.113.10 ;; Query time: 0 msec ;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP) $ dig +short api.example.com @ns1.example.com lb-prod-eu.example.net. 198.51.100.7 <- authoritative says the NEW address; the cache above still has the old one for 47 s
Authoritative vs cached: dig @server and +trace
dig @ns1.example.com api.example.com asks the authoritative server directly — the answer is the truth as of now, with aa set and full TTLs. If it disagrees with your default resolver, the record changed and a cache is serving the old value; the remaining TTL tells you for how long. If it agrees, the problem is not staleness. dig @8.8.8.8 and dig @1.1.1.1 give two more independent caches for comparison; disagreement between public resolvers is staleness, agreement with the authoritative is fine.
dig +trace api.example.com performs the recursive walk itself from the root servers, printing each delegation. It shows a broken delegation (the TLD points at name servers that do not answer or do not know the zone — SERVFAIL at the resolver) and lame or mismatched NS records. It also tells you which authoritative servers exist so you can query each one; if ns1 and ns2 disagree, a zone transfer or deployment is half-done. The stale-cache challenge in this module is that incident end to end.
dig +short name→ just the answer.dig name ANYis often refused; ask for types explicitly (A,AAAA,CNAME,MX,TXT,NS,SOA).dig -x 203.0.113.10→ reverse lookup (PTR). Missing PTRs break some mail and logging, nothing else.- Lower the TTL to 60 s a day before a planned change; raise it back after. Nothing shortens a TTL that is already cached.
Split-horizon, VPN and DoH: why browser and curl disagree
Corporate and cloud DNS is frequently split-horizon: the same name resolves to a private address from inside the network (or over VPN) and a public address from outside. Connecting the VPN typically pushes a resolver and search domains; disconnecting it restores the old ones — but the OS cache may keep answers from the previous world for their TTL, so internal.example.com keeps resolving to 10.0.4.2 after you disconnect, then fails. resolvectl status / scutil --dns show which resolver is active per interface and per domain.
Browsers add their own layer. Firefox uses DNS-over-HTTPS to a configured provider by default in some regions, and Chrome "upgrades" to DoH when the system resolver’s operator supports it. A DoH-enabled browser never consults /etc/hosts or the VPN-pushed resolver — so the browser sees the public answer while curl, which calls getaddrinfo() and therefore the OS chain, sees the private one. curl --doh-url reproduces the browser’s view; the browser’s about:networking#dns / chrome://net-internals/#dns show its cache. When two clients on the same machine disagree, the question is always "which resolver did each one use?".
Containers and Kubernetes add a fourth resolver: pods resolve through kube-dns/CoreDNS with search domains that turn api into api.default.svc.cluster.local, and a ndots:5 default that sends external names through five search-domain attempts before trying them as-is — slow, and a source of NXDOMAIN storms. Kubernetes Networking, Just Enough covers it.
- Same machine, different answers → different resolvers. Check the SERVER line, the browser’s DoH setting,
/etc/hosts, VPN state. - A name that works on VPN and fails off it is split-horizon: the private answer has no public route.
Negative caching
NXDOMAIN is cached too. RFC 2308 says a resolver may cache a negative answer for the minimum of the SOA record’s TTL and its MINIMUM field — often 300–3600 s, sometimes much longer. The classic incident: a developer tests new-service.example.com before the record exists, gets NXDOMAIN, creates the record, and then cannot resolve it for an hour from that machine or that resolver, while a colleague who never looked it up resolves it instantly. The fix is to query a resolver that has not seen the failure, or flush the one that has; the prevention is to create the record before anyone queries it.
Negative caching also means a resolver outage leaves a scar: if the authoritative servers were unreachable and the resolver returned SERVFAIL, that is typically not cached long (seconds), but a spurious NXDOMAIN from a misconfigured zone is, for the SOA minimum.
- Negative TTL = min(SOA TTL, SOA MINIMUM). Check it with
dig example.com SOA. - Flushing your own cache does not flush the recursive resolver’s; ask a different resolver or wait.
Key points
- Ask "who answered?" first: browser cache, hosts file, OS cache, recursive resolver, authoritative — each caches independently for the TTL.
diggives status, flags (aa= authoritative), the CNAME → A chain and a TTL that counts down when cached.dig @authoritativeanddig +traceshow the truth and the delegation path; compare with@8.8.8.8to find stale caches.- "Propagation" is TTL expiry in many caches at once; lower the TTL before a change, not after.
- Browser and curl disagree when they use different resolvers — DoH, VPN-pushed resolvers, split-horizon, hosts file.
- NXDOMAIN is cached for the SOA minimum; looking up a name before it exists poisons your resolver for that long.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why so many caches?
Every layer wants to avoid a network round trip it did recently; DNS is on the critical path of every connection, so each participant caches independently. The price is that a change is visible at different times from different places.
▸Why cache a negative answer at all?
Typos and misconfigured clients generate enormous volumes of lookups for names that do not exist; without negative caching every one would hit the authoritative servers. The SOA MINIMUM is how the zone owner bounds the damage.
▸Why do browsers bypass the OS resolver?
DoH encrypts queries to a resolver the browser trusts, defeating on-path snooping and ISP manipulation. It also defeats the corporate split-horizon setup the OS was configured for — a deliberate trade-off.
How it fails
What the failure looks like from inside real software.
- Changing a record with a one-day TTL during an incident and waiting a day for the old address to age out of every cache.
- Looking up a name before creating it, then debugging NXDOMAIN for the negative-cache TTL while the record exists.
- A stale
/etc/hostsentry from a past debugging session that silently overrides DNS for one engineer. - Firefox with DoH resolving a split-horizon name to the public address while
curlon the same laptop resolves the private one. - Kubernetes
ndots:5turning every external lookup into five NXDOMAINs before success, making "DNS is slow" a per-request 20 ms tax. - Half-deployed authoritative servers (
ns1updated,ns2not) so answers flip depending on which one the resolver picked.