Why this matters
dig is the tool you reach for in every DNS problem, and its output is dense. Most wrong conclusions come from reading only the answer and skipping the header: "no such name" and "name exists, but not that kind of record" look almost the same. This lesson reads the whole thing.
What you need to know already: 8.16 - resolvers, the stub at 127.0.0.53, authoritative servers, TTLs.
dig (domain information groper) sends one DNS question and prints the whole reply. dig NAME asks the server from resolv.conf; dig @SERVER NAME asks a server you choose.
A full answer, line by line
$ dig example.com
; <<>> DiG 9.20.11-1ubuntu2-Ubuntu <<>> example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 43522
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;example.com. IN A
;; ANSWER SECTION:
example.com. 86400 IN A 93.184.216.34
;; Query time: 31 msec
;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP)
;; WHEN: Wed Sep 23 10:12:01 UTC 2026
;; MSG SIZE rcvd: 56
Lines starting with ; are comments dig adds. The parts that matter:
status: NOERROR- the most important word on the screen. More below. (idjust matches the reply to the question.)flags: qr rd ra-qrthis is a reply;rd"recursion desired" (dig asked the server to do the whole chain);ra"recursion available" (the server does that).aa(authoritative answer) appears only when you ask the zone's own server.- counts - how many records in each section below.
- OPT PSEUDOSECTION - technical options (EDNS).
udp: 65494is a telltale that you are talking to systemd-resolved's stub. Otherwise skip it. - QUESTION - what was asked. The trailing dot in
example.com.means "the full name, ending at the root" - a fully qualified name. - ANSWER - one record per line: name, TTL in seconds, class (
IN= internet, always), type (A= an IPv4 address), data. - SERVER - who answered, and
#53= on port 53 (the numbered doorway DNS listens on; ports are from Ch 3).(UDP)is the kind of transport: UDP sends one message and hopes for a reply, no connection set up - fine for small questions like DNS. Always check SERVER: half of DNS confusion is asking a different server than you thought. - Query time - 0-1 ms usually means the answer came from a cache.
Ask again, and watch the TTL
+noall +answer = hide everything (+noall), then show only the answer section (+answer):
$ dig +noall +answer example.com
example.com. 7196 IN A 93.184.216.34
The TTL went down: this answer came from resolved's cache, which reports how long it will still keep the record (resolved caps cached TTLs at two hours, so 86400 became at most 7200). Ask the zone's own server and you get the full TTL and the aa flag:
$ dig @10.0.3.53 api.lab
...
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
...
api.lab. 30 IN A 10.0.3.20
(ra is missing: a server that only holds its own zone does not do the chain for others.) A TTL that counts down is a cached copy; a TTL that stays at its full value on every query comes from the source. That one observation answers "is this a cache problem?".
The status codes
NOERROR the server answered. Check ANSWER: it may still be empty!
NXDOMAIN the NAME does not exist (for any type)
SERVFAIL the resolver tried and failed: the zone's servers are down or
unreachable, or the chain is broken
REFUSED this server will not answer you: it does not hold that zone and
will not look it up for you
NXDOMAIN
$ dig nothing.lab
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 18231
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
...
;; AUTHORITY SECTION:
lab. 300 IN SOA ns1.lab. hostmaster.lab. 2026092301 7200 3600 1209600 300
The SOA record in AUTHORITY is the zone saying "I own lab., and there is no such name" (SOA = start of authority, the zone's own info record; 8.22 reads it field by field). Its last number (300) is how long resolvers will remember this negative answer.
NODATA: NOERROR with nothing in it
$ dig AAAA api.lab
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 5120
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
Record types go before or after the name. AAAA asks for an IPv6 address. The name exists; it just has no AAAA record. People call this NODATA. Scripts that check only the status miss it. It matters because programs ask for AAAA and A at the same time, and a slow or broken AAAA answer can delay every connection.
SERVFAIL
$ dig broken.example
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 772
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
Your resolver is fine; it could not get an answer from the zone. Next step: dig +trace (below) to see where the chain breaks, or ask the zone's servers directly with @.
REFUSED
$ dig @10.0.3.53 example.com
;; ->>HEADER<<- opcode: QUERY, status: REFUSED, id: 3117
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
You asked the lab zone's own server about a zone it does not hold. Common when someone points a box's resolv.conf at such a server instead of a resolver.
No answer at all
$ dig @10.0.3.99 api.lab
;; communications error to 10.0.3.99#53: timed out
;; communications error to 10.0.3.99#53: timed out
;; communications error to 10.0.3.99#53: timed out
; <<>> DiG 9.20.11-1ubuntu2-Ubuntu <<>> @10.0.3.99 api.lab
; (1 server found)
;; global options: +cmd
;; no servers could be reached
Three tries, five seconds each: fifteen seconds. Nothing answered on port 53 - wrong IP, the server is down, or a firewall drops it. Add +time=1 +tries=1 (wait 1 second, try once) when you are probing many servers.
Exit codes - a scripting trap
$ dig nothing.lab >/dev/null; echo $?
0
$ dig @10.0.3.99 api.lab >/dev/null; echo $?
9
dig exits 0 for NXDOMAIN and SERVFAIL - it did receive a DNS reply. It exits 9 only when no server replied. A health check written as dig name && echo ok (Ch 6) is always "ok". Check the answer instead: [ -n "$(dig +short name)" ].
Getting only what you need
$ dig +short example.com
93.184.216.34
$ dig +short www.example.com
example.com.
93.184.216.34
$ dig +noall +answer www.example.com
www.example.com. 86400 IN CNAME example.com.
example.com. 86400 IN A 93.184.216.34
www.example.com is a CNAME - an alias that says "look up example.com instead" (8.22). +short loses the TTLs and the status - an NXDOMAIN just prints nothing. +noall +answer keeps the records with their TTLs and is the best default for reading. Other record types:
$ dig +short MX example.com
10 mail.example.com.
$ dig +short NS example.com
a.iana-servers.net.
b.iana-servers.net.
$ dig +short TXT example.com
"v=spf1 -all"
$ dig +short SRV _postgresql._tcp.db.lab
0 5 5432 db.lab.
$ dig -x 8.8.8.8 +short
dns.google.
MX = mail server, NS = the zone's servers, TXT = free text, SRV = where a service runs (8.22 explains each). -x does a reverse lookup, address to name: dig builds the special name 8.8.8.8.in-addr.arpa. and asks for its PTR (pointer) record.
+trace: follow the chain yourself
+trace makes dig walk the chain itself, from the root down, instead of asking a resolver:
$ dig +trace github.com
; <<>> DiG 9.20.11-1ubuntu2-Ubuntu <<>> +trace github.com
;; global options: +cmd
. 517521 IN NS a.root-servers.net.
. 517521 IN NS b.root-servers.net.
;; Received 239 bytes from 127.0.0.53#53(127.0.0.53) in 1 ms
com. 172800 IN NS a.gtld-servers.net.
com. 172800 IN NS b.gtld-servers.net.
;; Received 1170 bytes from 198.41.0.4#53(a.root-servers.net) in 24 ms
github.com. 172800 IN NS dns1.p08.nsone.net.
github.com. 172800 IN NS ns-1283.awsdns-32.org.
;; Received 340 bytes from 192.5.6.30#53(a.gtld-servers.net) in 28 ms
github.com. 60 IN A 140.82.121.3
;; Received 56 bytes from 198.51.44.8#53(dns1.p08.nsone.net) in 31 ms
Each block is one step: what that server said, then Received ... from who said it. First the list of root servers, then the root points to the com servers, those point to github.com's servers, and those answer. Handing a name down like this is called delegation. +trace skips every cache, so it shows what the authoritative servers say right now. If +trace gives the new IP and plain dig the old one, it is caching.
It only works for names the public root knows:
$ dig +trace api.lab
...
lab. 86400 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2026092300 1800 900 604800 86400
;; Received 104 bytes from 198.41.0.4#53(a.root-servers.net) in 24 ms
The root has never heard of a lab TLD. Internal zones are served by internal servers that your resolver is set up to use. For those, go straight to the authoritative server with @.
host and nslookup
Two older tools that ask the same questions with friendlier output:
$ host example.com
example.com has address 93.184.216.34
example.com has IPv6 address 2606:2800:21f:cb07:6820:80da:af6b:8b2c
example.com mail is handled by 10 mail.example.com.
$ nslookup www.example.com
Server: 127.0.0.53
Address: 127.0.0.53#53
Non-authoritative answer:
www.example.com canonical name = example.com.
Name: example.com
Address: 93.184.216.34
"Non-authoritative answer" = it came from a resolver (usually a cache), not the zone's own server. Both tools apply the resolv.conf search list (8.27), which dig does not - so host orders may work while dig orders says NXDOMAIN. That is not a DNS bug; the two tools asked for two different names.
What you can now do
- Read status, flags, sections, TTL and SERVER in any dig output.
- Tell NOERROR, NODATA, NXDOMAIN, SERVFAIL, REFUSED and a timeout apart.
- Use
+short,+noall +answer,@server,-xand+trace.