DNS gets blamed first and debugged last. The symptom is always the same — the page won't load, the API times out, the container can't reach its database — and the cause can be anywhere between your laptop's cache and a TLD server. So people run every command they remember, in random order, and learn nothing.
The move that actually works is duller: classify the symptom first, then pick one command for that class. Six symptoms cover almost every DNS failure I have run into in twenty-odd years of operations work.
"DNS is broken"
│
├─ NXDOMAIN ──────────────── The name doesn't exist, or the delegation hasn't propagated
├─ SERVFAIL ──────────────── Some server in the chain refused to answer
├─ Timeout ───────────────── The recursor is down, or port 53 is blocked on the path
├─ CNAME won't resolve ───── A loop, or a CNAME sitting on top of an A record
├─ Record won't change ───── TTL hasn't expired yet
└─ DNSSEC fails ──────────── The signature chain is broken upstream
Start at the top of that list, not at the top of your memory.
SERVFAIL: try +cd before you chase anything
This is the one worth reading the article for.
SERVFAIL is DNS's version of a 500. The server refuses to answer and won't say why. Half the time it is not a broken server at all — it is DNSSEC validation failing, and the resolver is being honest about it. Two commands tell them apart:
dig @8.8.8.8 example.com # SERVFAIL
dig @8.8.8.8 example.com +cd # validation off: NOERROR, correct IP
If the second one returns the right answer, the record is fine and the chain is broken. Look at the two ends of the chain:
dig example.com DS +noall +answer # at the parent (registrar / TLD)
dig example.com DNSKEY +noall +answer # at the authoritative
Different key IDs mean the registrar never synced the DS record after a key rotation. That gets fixed at the registrar, not on your server. You can spend an afternoon restarting BIND and never touch the actual problem.
If both queries return SERVFAIL, the authoritative really is down and you should go chase connectivity instead.
NXDOMAIN: three questions, in order
NXDOMAIN means "this name does not exist". Ask it three times, from the outside in:
dig example.com # your local recursor
dig @ns1.example.com example.com # the authoritative, cache bypassed
dig @a.gtld-servers.net example.com NS # the delegation at the TLD
-
NOERRORwith a non-empty answer section → the name exists and your resolver was lying. Flush it. -
NOERRORwith an empty answer section → the name exists but has no A/AAAA record. Add one. -
NXDOMAINfrom the authoritative → the name genuinely doesn't exist. Check the registrar.
The classic false alarm is a domain registered ten minutes ago. The registrar console says "registered", the authoritative returns NXDOMAIN, and nothing is actually wrong: the delegation to the TLD hasn't been published yet. That has taken anywhere from 15 minutes to six hours in cases I have handled. Don't touch your resolver config; check the TLD first.
Timeout: compare resolvers, don't guess
One timeout tells you nothing. Three timeouts tell you where the problem is:
dig @8.8.8.8 example.com +time=3 +tries=1
dig @1.1.1.1 example.com +time=3 +tries=1
dig @223.5.5.5 example.com +time=3 +tries=1
dig example.com +time=3 +tries=1 # whatever your host uses
- All four fail → the path is blocked, or port 53 is filtered somewhere.
- Only the public ones fail → your internal recursor is down. Check
systemctl status systemd-resolvedorsystemctl status bind. - Public works, internal is slow → the internal recursor is misconfigured, not dead.
When exactly one machine is affected and its neighbours on the same VLAN are fine, stop looking at the network. Check the local resolver service. A segfaulted systemd-resolved looks exactly like a network fault from the application's point of view, and sudo systemctl restart systemd-resolved fixes it in two seconds:
systemctl status systemd-resolved
sudo systemctl restart systemd-resolved
sudo resolvectl flush-caches
CNAME loops, and the trap next to them
dig example.com +noall +answer +comments
A loop looks like this, and resolvers give up after eight CNAME hops:
example.com. 300 IN CNAME www.example.com.
www.example.com. 300 IN CNAME example.com.
The neighbouring trap is quieter: a CNAME cannot share a name with any other record. This is invalid and some providers will happily save it anyway.
example.com. 300 IN CNAME cdn.example.com.
example.com. 300 IN A 1.2.3.4
Split it into two names — app.example.com pointing at lb.example.com, with the A record on lb.example.com — and the problem disappears.
"I changed the record and nothing happened"
That's TTL, almost every time. Read it out of the answer section:
dig example.com | head -20
# example.com. 86400 IN A 1.2.3.4
# ^^^^^ 86400 seconds = 24 hours. You wait 24 hours.
The change procedure that avoids this, in this order:
- Lower the TTL to 60 seconds.
- Wait 60 seconds.
- Change the record.
- Wait for propagation.
- Put the TTL back.
Do steps 1 and 3 in the other order and the change is a no-op for a day. Then compare two independent resolvers to see who is stale:
dig @8.8.8.8 example.com
dig @1.1.1.1 example.com
Same answer from both → wait out the TTL. Different answers → one of them is stale, flush it. Both stale → the authoritative never changed; go back to the registrar.
Cache flush commands, by platform:
sudo systemctl restart systemd-resolved # Linux, systemd-resolved
sudo kill -SIGHUP $(pgrep dnsmasq) # Linux, dnsmasq
ipconfig /flushdns # Windows
sudo dscacheutil -flushcache # macOS
sudo killall -HUP mDNSResponder # macOS
DNSSEC: read the flags, not the error message
dig example.com +dnssec +noall +comments
dig example.com +cd +noall +comments
An ad bit in the flags line means validation succeeded. SERVFAIL with +dnssec and NOERROR with +cd means the answer is valid but unsigned somewhere up the chain. If the DS and DNSKEY key IDs don't match, the break is at the delegation — escalate to the registrar with the command output rather than rebuilding the zone.
Cheat sheet
| Command | What it gives you |
|---|---|
dig +short example.com |
The answer, one line |
dig example.com A |
A record |
dig example.com AAAA |
IPv6 record |
dig example.com MX |
Mail routing |
dig example.com TXT |
SPF / DMARC / DKIM |
dig example.com NS |
Authoritative nameservers |
dig @8.8.8.8 example.com |
Ask a specific resolver |
dig @ns1.example.com example.com |
Ask the authoritative, cache bypassed |
dig example.com +trace |
Iterative walk from the root |
dig example.com +noall +answer |
Answer section only |
dig example.com +dnssec |
Request signatures |
dig example.com +cd |
Skip validation |
dig covers about 90% of this. host is fine in scripts. nslookup is what Windows gives you and it shows you the least — if you can dig, don't nslookup.
Still broken? Drop to packets
sudo tcpdump -nn -i any -s 0 port 53 -c 50
sudo tcpdump -nn -i any port 53 and host 8.8.8.8
Query leaves, no response comes back → the path or a middlebox is dropping it. Response comes back but the application never sees it → it's the local resolver, not the network.
I keep a longer version of this with the full decision tree, conditional forwarding configs for BIND and dnsmasq, the Docker 127.0.0.11 resolver behaviour, and three production post-mortems here: DNS troubleshooting: six-symptom decision tree. It also covers the DNSSEC caveat I skipped above — Cloudflare, Route 53 and Namecheap each handle key rotation slightly differently.
One more thing worth bookmarking: dig example.com +trace is the single most informative command in this article, and almost nobody runs it before restarting something. The trace tells you which hop is broken. Restarting tells you nothing.
Top comments (0)