When a name will not resolve, the answer to "why" is almost always sitting in plain sight in the output of one command, if you know how to read it. The first moves, checking the gateway and then splitting the problem into "network" or "DNS," are in networks · The troubleshooting toolkit, and what a resolver is lives in networks · DHCP & DNS. This chapter starts where those stop: every line of a dig answer, the flags that matter, how to ask the server that actually knows, how to follow a delegation down the tree, and a field guide to the five ways DNS says no.
Common mix-up: an empty answer is not NXDOMAIN. If you ask for a name's IPv6 address and get status: NOERROR with ANSWER: 0, the name exists and simply has no record of that type. That is NODATA, and it has no response code of its own: you recognize it by the empty answer section and the SOA record in the authority section (RFC 2308). NXDOMAIN, code 3, means the name itself does not exist. Mixing them up sends you to fix the wrong thing.
dig ("domain information groper") ships with BIND, the DNS software from ISC, and is on nearly every Mac and Linux machine. Its virtue is that it hides nothing. Your browser, your operating system and ping all ask the DNS through layers of caching and retry logic and tell you only "it worked" or "it didn't." dig sends exactly one question to exactly one server and prints the reply it got back, almost byte for byte, with every field labeled.
Here is a complete answer. Everything below this point in the chapter is simulated, with reserved names (example.test, example.net) and documentation addresses (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24), but the format is exactly what BIND 9's dig prints.
; <<>> DiG 9.18.28 <<>> www.example.test A ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 41865 ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 1232 ;; QUESTION SECTION: ;www.example.test. IN A ;; ANSWER SECTION: www.example.test. 2400 IN A 192.0.2.10 ;; Query time: 2 msec ;; SERVER: 192.0.2.53#53(192.0.2.53) (UDP) ;; WHEN: Thu Oct 22 10:20:00 UTC 2026 ;; MSG SIZE rcvd: 61
Read it in the order it is printed and ask one question of each part.
The header line: did it work? status is the response code, the single most important word on the page. opcode: QUERY is nearly always what you see. The id is a random 16-bit number dig put in its question; the reply has to carry the same one or dig ignores it.
The flags: who answered, and how? These are one-bit switches in the header, and they are how you tell a cached answer from a source answer and a validated answer from an unchecked one. The counts after them say how many records are in each section.
The OPT pseudosection: what was negotiated? Modern DNS rides on EDNS (RFC 6891), an extension carried as a fake record in the additional section. That is why ADDITIONAL says 1 even though no additional records are printed: the one is the OPT record. It says how large a UDP reply the sender can accept (here 1,232 bytes), whether DNSSEC records were wanted (the do flag) and, when something fails, an Extended DNS Error with a reason.
The sections: what did it say? The question is echoed back with a leading semicolon, because it is not a record. Then each record in the answer, authority and additional sections, one per line: name, TTL, class, type, data.
The footer: who said it, and how fast? SERVER is the address that actually replied and whether it came over UDP or TCP. A query time of 1 or 2 milliseconds almost always means a cache near you; tens of milliseconds means a trip across the internet; seconds means something timed out and was retried.
| Flag | Name | Set means | What it tells you when debugging |
|---|---|---|---|
| qr | query response | This message is a reply | Always set in what dig prints; it is how the server marks its answer |
| aa | authoritative answer | The server answering holds the zone | You reached the source. No aa from a server you think is authoritative is a red flag (a lame server, or the wrong server) |
| tc | truncated | The reply did not fit in UDP | dig retries over TCP automatically; if TCP is blocked, the lookup fails (RFC 7766) |
| rd | recursion desired | The asker wants the server to go and find the answer | Set by default; +norecurse clears it, which is what you want when asking an authoritative server |
| ra | recursion available | This server will recurse for you | Set on resolvers, clear on authoritative servers. Missing on a resolver means it refused to recurse for you |
| ad | authentic data | The resolver validated the answer with DNSSEC | Only meaningful from a validating resolver you trust. Absent on an unsigned zone; absent with SERVFAIL on a broken one |
| cd | checking disabled | The asker said "do not validate for me" | +cd sets it. If a query fails without it and succeeds with it, the fault is DNSSEC |
The 12-byte header is the subject of Chapter 2. The short version: the third and fourth bytes are a 16-bit word holding qr, a 4-bit opcode, aa, tc, rd, ra, a reserved bit, ad, cd and the 4-bit RCODE (RFC 1035 §4.1.1, with ad and cd added by RFC 4035 §3.2). Four bits of RCODE only reach 15, so EDNS adds eight more high bits in the OPT record (RFC 6891 §6.1.3); that is how a code like BADVERS (16) exists. dig glues the pieces together and prints the name.
The do ("DNSSEC OK") bit is not a header flag at all. It lives in the OPT record's flags, which is why dig prints it on the EDNS line and not on the flags line.
Pick a sample. Every underlined word is live: hover it, tap it, or tab into the output and use the arrow keys, and the box underneath explains exactly that piece, with any arithmetic done for you (how long a TTL really is, how the negative-cache time is worked out, whether the reply would have fitted in UDP). Then switch on the quiz and say what the output is telling you. The samples are generated by the same small simulator that drives the diagnosis game further down, so sizes and counts are computed, not typed in. All data is simulated.
Start with the word after status:, then the flags.
Keyboard: focus the output, then ← → step through parts, ↑ ↓ jump a line. Score so far: 0 of 0.
A plain dig www.example.test asks whichever resolver your machine is configured with, and that resolver answers from its cache whenever it can. That is the right first question, because it is the answer your users get. It is almost never the last one. The single most useful habit in DNS debugging is to name the server you are asking with @, and to ask several in turn: your resolver, a different resolver, and each authoritative server for the zone. Where their answers differ is where the fault is.
When you ask an authoritative server, add +norecurse. You want to know what that server itself holds, not what it might go and fetch if it happens to also be a resolver. A healthy reply has aa set, no ra, and the TTL exactly as configured in the zone rather than a number counting down.
| Option | What it does | Reach for it when |
|---|---|---|
| @server | Send the question to this server instead of the system resolver | Always, once you are past the first look. Compare servers |
| +norecurse | Clear rd: "tell me only what you hold" | Asking an authoritative server, or peeking at a resolver's cache |
| +trace | Do the resolution yourself, from the root down, printing each referral | The delegation itself is in doubt: wrong NS, lame server, missing glue |
| +short | Print only the answer data | Scripts and quick checks. It hides the status, so never use it to diagnose a failure |
| +dnssec | Set the do bit and show RRSIG and other DNSSEC records | Checking that a zone is signed and its signatures are current |
| +cd | Ask the resolver not to validate | A SERVFAIL you suspect is DNSSEC: if +cd fixes it, it is |
| -x addr | Reverse lookup: builds the in-addr.arpa or ip6.arpa name for you | Checking PTR records (Chapter 7) |
| +tcp | Use TCP from the start | Large answers, or testing whether TCP port 53 gets through a firewall |
| +multiline | Split long records over several lines with comments | Reading SOA timers and DNSKEY key tags |
delv, also from BIND, resolves a name and then validates the DNSSEC chain itself, printing either ; fully validated or a reason it could not. It is the tool to use when you need to know why validation failed rather than just that a resolver returned SERVFAIL.
Resolve-DnsName is the PowerShell equivalent on Windows. Resolve-DnsName www.example.test -Server 192.0.2.1 -Type A asks a named server; -NoRecursion clears rd; -DnsOnly skips the hosts file and other local name sources; -DnssecOk asks for signatures. Its output is a table rather than raw sections, but the same reasoning applies.
nslookup is everywhere, and it will do in a pinch, but it hides the things you most need. It does not print the flags, so you cannot see aa, ad or tc. It reports a NODATA reply and an NXDOMAIN in ways that are easy to confuse. Its own reverse lookup of the server's address prints a confusing error at the top when that address has no PTR record, which looks like a failure when nothing is wrong. Use it to confirm, not to diagnose.
The SERVER line is the honest answer, but only for the query dig made. Your applications may use something else. On macOS, scutil --dns lists every resolver configuration, including per-domain ones a VPN adds. On Linux with systemd-resolved, resolvectl status shows which servers each network link uses, and plain dig there talks to the local stub at 127.0.0.53, which forwards on. On Windows, Get-DnsClientServerAddress lists them per adapter, and Get-DnsClientNrptPolicy shows name-based routing rules that VPNs install.
This matters most for split horizon (Chapter 10): the right answer may exist only on a resolver your laptop is not using.
When a lookup fails, the first job is to classify the failure, because each kind points somewhere different. The response code is a four-bit number in the header (RFC 1035 §4.1.1, registry kept by IANA), and only a handful of its values show up in practice.
| What dig shows | Code | Means | Look next at |
|---|---|---|---|
| NOERROR + answer | 0 | Here is your data | Is it the data you expected, from the server you meant? Check TTL and SERVER |
| NOERROR, ANSWER: 0 | 0 | NODATA: the name exists, but has no record of this type | The type you asked for. An AAAA query on an IPv4-only name is the usual case |
| NXDOMAIN | 3 | The name does not exist, and nothing exists below it (RFC 8020) | Spelling, the zone file, a stale negative cache, or a view that lacks the name |
| SERVFAIL | 2 | The resolver tried and could not get a trustworthy answer | DNSSEC (try +cd), unreachable or lame authoritative servers. Read the EDE line |
| REFUSED | 5 | This server will not answer this question for you | You are asking an authoritative server for a zone it does not serve, or a resolver that does not serve your network |
| FORMERR | 1 | The server could not parse the question | Old or broken middleboxes and servers, often choking on EDNS |
| timed out | none | No reply at all | Reachability: the address, a firewall, port 53, or a server that is down |
Negative answers, NXDOMAIN and NODATA alike, carry the zone's SOA record in the authority section. That is not decoration. RFC 2308 says a resolver may cache the "no" for the lower of that SOA record's TTL and its last field, MINIMUM, and the TTL dig prints on the SOA line is how many seconds of that are left. When someone creates a record and a colleague still gets NXDOMAIN, this number is how long they will keep getting it (Chapter 4 has the full story).
; NODATA: the name exists, there is just no AAAA record ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 3301 ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1 ;; AUTHORITY SECTION: example.test. 3600 IN SOA ns1.example.test. hostmaster.example.test. 2026102201 7200 900 1209600 3600 ; NXDOMAIN: no such name at all (a typo with four w's) ;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 3302 ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1 ;; AUTHORITY SECTION: example.test. 3600 IN SOA ns1.example.test. hostmaster.example.test. 2026102201 7200 900 1209600 3600
For thirty years SERVFAIL meant only "something went wrong," and the something could be an expired signature, a server that did not answer, or a resolver policy. RFC 8914 (2020) added an EDNS option that carries a numeric reason and an optional line of text. dig prints it in the OPT pseudosection, and resolvers that support it, including the big public ones, fill it in. A few you will meet:
| EDE | Name | Typical cause |
|---|---|---|
| 3 | Stale Answer | The answer came from an expired cache entry because the source could not be reached (serve-stale) |
| 6 | DNSSEC Bogus | Validation failed: a signature did not check out |
| 7 | Signature Expired | The zone's RRSIGs ran past their end date: re-signing stopped |
| 9 | DNSKEY Missing | A DS at the parent names a key the child does not publish: the classic "moved provider, left the DS behind" |
| 15–18 | Blocked, Censored, Filtered, Prohibited | Resolver policy, not a fault in the zone |
| 22 | No Reachable Authority | None of the zone's servers answered usefully |
| 23 | Network Error | A network-level failure reaching an authoritative server |
One combination surprises people: status: NXDOMAIN together with a CNAME record in the answer section. That is correct. The response code describes the last name in the chain, not the one you asked for. Your name existed and pointed elsewhere; the name it pointed to does not exist. Read the CNAME's target carefully: the fault is there.
The same logic applies to NODATA through a CNAME: NOERROR, the CNAME in the answer, and an SOA in the authority section that belongs to the target's zone, which may not be yours at all.
Every DNS fault is a disagreement: between what you think the zone says and what it says, between the parent and the child, between one cache and another. The method is to make those parties answer the same question side by side until you find the pair that disagree.
Your browser, your operating system, your router and your resolver each keep their own copy (Chapter 4 draws the stack). sudo killall -HUP mDNSResponder on macOS, resolvectl flush-caches with systemd-resolved and Clear-DnsClientCache or ipconfig /flushdns on Windows clear only the operating system's layer. A recursive resolver's cache can be cleared only by whoever runs it; some public resolvers offer a web form for one name. dig itself has no cache, which is one more reason to debug with it rather than with a browser.
A user reports a symptom. You have a laptop with dig and the network described in the case. Choose your next command from the menu; the simulator works out the reply from a model of the zone, its servers, its parent, three resolvers and their caches, and prints it in dig's format. Every word in the replies is explainable, exactly as in the reader. When you think you know, name the fault. Fewer commands and fewer wrong guesses score better. Everything is simulated: names under example.test, documentation addresses. Lines beginning ; note: are this page's annotations, not dig's.
Then point at any underlined part of its output.
A normal query hides the resolver's work. dig +trace does that work itself, in front of you. It asks your resolver only for the root servers' names, then asks a root server about your name and prints the referral it gets (the NS records for test), asks one of those servers and prints the next referral (the NS records for example.test, plus the DS record if the zone is signed), and finally asks one of the zone's own servers. After each step it prints a ;; Received … bytes from … line saying which server it heard from. Chapter 3 explains the walk itself; here the point is reading it.
Two things make a trace powerful. It shows the delegation as the parent publishes it, which is the version resolvers actually use to find you, and which your own zone file cannot fix. And it ignores every cache, so a trace that works while users fail points at caches, and a trace that fails points at the zone or its delegation.
It has limits. It picks one server at each level, at random, so a fault on one of two servers shows up only some of the time: run it more than once, or ask each server directly. It does not validate DNSSEC; use delv for that. And it does not follow CNAMEs into other zones.
The instrument below runs the same trace against a healthy zone and a broken one and lines them up. To keep the comparison fair, both traces ask the zone's servers in the same order, starting with ns2.
Choose a fault. The simulator builds two copies of the example.test world, identical except for that one fault, runs dig +trace +dnssec against each, and diffs the output line by line. Lines that differ are highlighted, the first difference is outlined, and the ladder shows which hop went wrong. The validation line underneath is what a validating resolver would conclude. Simulated data throughout.
Each of these is a real, common failure, shown on the simulated example.test zone. Each is also a case in the diagnosis game, where you can find it yourself. The theory behind each fault lives in its own chapter; this is how it looks from the outside.
In a zone file, a name without a final dot is relative: the zone's origin is appended. Someone types www IN CNAME web.example.test and the server reads it as web.example.test.example.test., which does not exist. The resolver follows the CNAME and gets NXDOMAIN, with your CNAME sitting in the answer section to show you the doubled name. RFC 1912 (§3.2) calls out exactly this. Fix: add the dot, bump the serial, reload.
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 7710 ;; ANSWER SECTION: www.example.test. 3600 IN CNAME web.example.test.example.test.
The parent lists ns1 and ns2; you moved off ns2 months ago and the zone's own NS records now say ns1 and ns3. Resolvers trust the parent's list to get started, so some of them ask ns2 first, get REFUSED or a non-authoritative reply, and try again elsewhere. Users see slow first loads; under load, the odd SERVFAIL. Ask each listed server directly with +norecurse and compare the parent's NS set with the child's (Chapter 6; RFC 1912 §2.8). Fix: update the delegation at the registrar.
A signed zone moves to a new DNS provider that does not sign it, or signs it with new keys, and nobody removes or replaces the DS record at the registrar. Every validating resolver now sees a parent saying "this zone is signed with key 2371" and a child that publishes no such key, and refuses the answer: SERVFAIL, EDE 9 (DNSKEY Missing). The provider's dashboard looks perfect because the authoritative servers are fine. +cd makes the error vanish, which is the tell. Fix: publish a DNSKEY that matches, or remove the DS (Chapter 11).
A CNAME says "this name is an alias, and has no other records." The zone apex must have SOA and NS records, so a CNAME there breaks the rules (RFC 1912 §2.4). Strict servers refuse to load such a zone; lenient ones serve it, and then a resolver asking for the apex's MX follows the CNAME into someone else's zone, finds no MX, and returns NODATA. The website works; mail bounces. Fix: an A/AAAA record at the apex, or a provider's flattening feature, or an HTTPS record (Chapter 5).
Someone looked up shop.example.test before it existed. Their resolver cached the NXDOMAIN for min(SOA TTL, MINIMUM), here an hour. You create the record; it works for you, because your resolver never asked; it keeps failing for them until the timer runs out. The SOA's TTL in their negative answer shows exactly how long is left (Chapter 4). Fix: wait, or flush that resolver; next time, lower the SOA MINIMUM before launches.
app.example.test exists only in the internal view of the office resolver. From home, with the VPN up, it does not resolve, because the VPN carries your traffic but did not change which resolver your laptop asks. Your resolver and the public authoritative servers both say NXDOMAIN, correctly, for the public view. Asking the office resolver by address answers at once. Check what your system actually uses (scutil --dns, resolvectl status, Get-DnsClientNrptPolicy) and fix the VPN's DNS settings (Chapter 10).
A zone's TXT records have grown with years of verification tokens, and the reply is now larger than the 1,232-byte UDP size most software advertises. The resolver answers with the tc flag set and dig prints ;; Truncated, retrying in TCP mode. A firewall on the office network allows only UDP on port 53, so the TCP retry times out. Small lookups work; big ones fail. RFC 7766 makes TCP support mandatory for DNS: the fix is to allow TCP/53, and secondarily to prune the records (Chapter 2).
In 1996 David Barr wrote down the configuration mistakes he kept seeing. Nearly every one is still on the list of things to check, and several of them are case files above.
| RFC 1912 | Mistake | What dig shows |
|---|---|---|
| §2.1 | A record and PTR that do not match, or a missing PTR | dig -x returns NXDOMAIN or a different name |
| §2.2 | SOA serial not increased after an edit | Secondaries never pick up the change: different answers from ns1 and ns2, same serial |
| §2.3 | Glue that is wrong or out of date | +trace shows the parent's glue address timing out |
| §2.4 | CNAME alongside other data, CNAME at the apex, MX or NS pointing at a CNAME | NODATA through a CNAME, or a zone the server refuses to load |
| §2.8 | Lame delegation | REFUSED, or an answer without aa, from a server the parent lists |
| §3.2 | Missing trailing dot | The zone's name appearing twice in a target |
For DNSSEC in particular, a picture beats a page of dig. DNSViz, a free web tool, queries every server at every level of the chain and draws the delegations, keys and signatures as a graph, with each problem flagged. It is the fastest way to confirm a DS/DNSKEY mismatch or an expired signature on a public zone. The same project publishes the command-line tool behind the site.