kmail.at
← learning

networking · difficulty ◆◆

dig — ask DNS the question, not the cache

When a hostname works on your laptop and dies on the server, dig tells you which of the three DNS layers lied — instead of shrugging and saying "DNS problem".

I asked the internet's fastest resolver for every record on my own domain and it refused with NOTIMP. That refusal turned out to be the most useful answer I got all week.

2026-10-05 · 6 min read

$ dig

What it does

dig sends a DNS query and prints what actually came back: the header (rcode, flags), the OPT pseudosection (EDNS buffer size, the Extended DNS Errors a resolver attaches), then QUESTION, ANSWER, AUTHORITY and ADDITIONAL sections, and finally the metadata humans argue about — query time, which server answered, how many bytes arrived. It is one binary from BIND 9 that speaks the whole protocol: any RR type (A, AAAA, MX, NS, TXT, SOA, CAA, DS, NSEC3), any class (IN, CH for version.bind), any port, TCP or UDP, DNSSEC with +dnssec, and DNS over TLS with +tls. Three modes cover every real task. Recursive mode (the default) asks your resolver and shows what a normal program sees. Authoritative mode (@ns.example.com +norecurse) shows exactly what the zone publishes, with no cache in the way. Trace mode (+trace) walks root, TLD and registrant nameserver yourself.

Why it matters

"DNS is broken" almost never means DNS is broken. It means one of three different machines gave a different answer: the zone owner's authoritative server, your resolver's cache, or the stub resolver on this host. dig is how you find out which. The header rcode separates NXDOMAIN (the name does not exist) from REFUSED (a policy said no) from SERVFAIL (something upstream failed) — three problems with three completely different owners. The flags separate a real answer from a cached ghost: aa means the server owns the zone, rd means you asked it to recurse, and the TTL is the number of seconds until the answer changes. When a record looks right but the user still cannot reach the box, the AUTHORITY section is the tell: an empty ANSWER carrying the root SOA is DNS for "that TLD does not exist".

Example

$ dig kmail.at

; <<>> DiG 9.18.39-0ubuntu0.24.04.7-Ubuntu <<>> kmail.at
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42902
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;kmail.at.			IN	A

;; ANSWER SECTION:
kmail.at.		3	IN	A	80.108.50.214

;; Query time: 0 msec
;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP)
;; WHEN: Mon Oct 05 09:05:30 CEST 2026
;; MSG SIZE  rcvd: 53

The first thing I run on any box. status: NOERROR plus one answer means the name resolves. SERVER 127.0.0.53#53 is systemd-resolved's stub on this host's loopback, which is why a stale entry can survive a browser reload — the machine that actually answered is not the nameserver on the first line of /etc/resolv.conf. A TTL of 3 means a cache entry about to be refetched, and MSG SIZE 53 is the entire packet once.

$ dig +short kmail.at MX
dig +short kmail.at NS
dig +short kmail.at TXT
10 mx02.mail.icloud.com.
10 mx01.mail.icloud.com.

ns1061.ui-dns.de.
ns1061.ui-dns.biz.
ns1061.ui-dns.org.
ns1061.ui-dns.com.

"v=spf1 include:icloud.com ~all"
"apple-domain=NJIIQYCRTgn7H7Gh"
"openai-domain-verification=dv-Awl1SI2qtF8iN5WE6zXMAVm6"
"apple-domain=LHNWZPmj9HF6Zi8J"
"google-site-verification=fCYSIbD1SeXD92cYPjNeuhlW6MCIx5JSMCAil4bFDcQ"

+short is the scriptable face of dig — one value per line and nothing else, so it drops straight into $( ), a cron check or a monitoring probe. Notice what it throws away: the MX list arrives as bare hostnames with their preference numbers, and TXT records keep their surrounding quotes because a TXT record is one or more character-strings. The SPF record ends in ~all, a softfail: mail from anywhere else gets marked, not rejected.

$ dig @ns1061.ui-dns.de kmail.at A +norecurse

; <<>> DiG 9.18.39-0ubuntu0.24.04.7-Ubuntu <<>> @ns1061.ui-dns.de kmail.at A +norecurse
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 36286
;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1220
;; QUESTION SECTION:
;kmail.at.			IN	A

;; ANSWER SECTION:
kmail.at.		60	IN	A	80.108.50.214

;; Query time: 12 msec
;; SERVER: 185.132.32.61#53(ns1061.ui-dns.de) (UDP)
;; WHEN: Mon Oct 05 09:05:31 CEST 2026
;; MSG SIZE  rcvd: 53

The cache-buster: point dig at the registrar's nameserver with +norecurse and you see the zone itself, not a copy. flags: qr aa — no rd, no ra — is the proof that this server owns the zone and answered from the zone file; the aa bit never appears in a recursive answer. The 60-second TTL is the zone's own, versus the 3 you saw a moment earlier through the resolver. '(2 servers found)' is dig quietly resolving one hostname to two addresses and querying one of them.

$ dig kmail.att +noall +comments
dig +short kmail.att; echo "exit=$?"
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 23407
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
exit=0

The typo check, and the trap it springs on scripts. kmail.att (one t) does not exist, so the rcode is NXDOMAIN and there is no ANSWER section at all — yet dig still exits 0, because 0 means 'a DNS response was received', not 'the answer was the one you wanted'. That is why [ "$(dig +short host)" ] is the correct guard and piping dig into grep is not. A black-holed server is worse: without bounds, dig waits out three tries per nameserver before giving up.

$ dig kmail.at ANY @1.1.1.1

; <<>> DiG 9.18.39-0ubuntu0.24.04.7-Ubuntu <<>> kmail.at ANY @1.1.1.1
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOTIMP, id: 58861
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; EDE: 21 (Not Supported)
;; QUESTION SECTION:
;kmail.at.			IN	ANY

;; Query time: 15 msec
;; SERVER: 1.1.1.1#53(1.1.1.1) (TCP)
;; WHEN: Mon Oct 05 09:05:31 CEST 2026
;; MSG SIZE  rcvd: 43

ANY is dead and Cloudflare says so politely: NOTIMP plus Extended DNS Error code 21, which is RFC 8482's answer to the amplification attack ANY used to enable. Two more tells in three lines — udp: 1232 instead of the 65494 the local stub advertises, and (TCP), because the query went out over TCP. Any inventory script still doing dig ANY has been quietly returning nothing since 2019.

$ dig -4 +trace kmail.at A 2>/dev/null | tail -n 6
;; Received 543 bytes from 194.0.25.10#53(r.ns.at) in 13 ms

kmail.at.		10800	IN	NS	ns1061.ui-dns.de.
kmail.at.		10800	IN	NS	ns1061.ui-dns.biz.
kmail.at.		10800	IN	NS	ns1061.ui-dns.org.
kmail.at.		10800	IN	NS	ns1061.ui-dns.com.
;; Received 53 bytes from 185.132.32.61#53(ns1061.ui-dns.de) in 12 ms

+trace ignores every resolver on the box and starts at the root, so each ';; Received' line names the server that took you one level deeper: the .at TLD server that delegated kmail.at, then the .de nameserver that finally returned the A record. I add -4 because on this host the IPv6 addresses of the .at TLD time out — without it the trace interleaves four 'communications error' lines per level, and the one line you want scrolls off the top.

Common flags

@server
query that server and skip /etc/resolv.conf. Takes an IP, a hostname (dig resolves it first, which is where '(2 servers found)' comes from) or a port override like @1.1.1.1 -p 53.
+short
terse mode: answers only, one per line, no header and no metadata. Always global, so it cannot be set globally and then turned off for one lookup in a multi-query command.
+trace
walk the delegation from the root down instead of asking a resolver, printing every referral. Incompatible with +short, so pipe it instead: dig +trace host | tail -20.
+norecurse
sets rd=0 — 'do not use your cache, answer from the zones you own'. Pair it with @ns.of.the.domain or you will get a bare NOERROR with zero answers from a server that simply declined to look.
+noall +answer
selective output. +noall silences everything, then +answer, +comments, +question, +stats and +authority each print one section back. This is the building block of every dig parser ever written.
+yaml
dumps the whole exchange as YAML for jq-able automation — timestamps, socket family, EDNS options, every section. It is an alias of +json, which the usage text does not advertise and which this build rejects as an unknown option.
+dnssec
request DNSSEC records and set the DO bit. Skip it and a validator's ad flag never appears, leaving a signed NOERROR indistinguishable from an unsigned one.
+tls, +https
DNS over TLS on 853, DNS over HTTPS on 443, for networks where plain 53 is blocked. Both want a server URL rather than an IP: dig +https @https://cloudflare-dns.com/dns-query fails with 'not found'.

History

1988 — a research tool with a bad backronym

dig was written by Steve Hotz at the Information Sciences Institute and shipped in BIND 4. The FreeBSD 2.2.8 man page still credits him — 'Steve Hotz hotz@isi.edu' — and thanks nslookup's author Andrew Cherenson for the routines dig borrowed. That page explains the name as 'domain information groper': a backronym invented after the fact, the way plenty of 1980s tool names were. Michael Sawyer later rewrote the program and ISC maintains the BIND 9 version. ISC deleted the expansion from the BIND 9 man page in 2017, so today's man dig describes what the tool does without ever claiming the letters stand for anything.

1998 — the bug report that became a compliment

The same FreeBSD 2.2.8 page carries a BUGS section worth framing: 'Dig has a serious case of creeping featurism -- the result of considering several potential uses during it's development. It would probably benefit from a rigorous diet.' It goes on to complain that dig 'does not consistently exit nicely (with appropriate status) when a problem occurs somewhere in the resolver ... This is particularly annoying when running in batch mode.' The diet never happened — +yaml, +cookie, +padding, +subnet, +tls, +https and EDE parsing all now live in the same binary — but the exit-status warning is still the right advice in 2026: exit 0 means a DNS response arrived, not that the name resolved.

Fun facts

Pros & cons

pros

  • + One binary covers every real DNS job — recursive diagnosis, authoritative verification, DNSSEC, zone transfers, DoT and DoH — and it is present on nearly every Linux box because systemd-resolved, NetworkManager and resolvconf all lean on it
  • + +short, +noall/+answer, +yaml and batch files make it scriptable for humans and parsers alike, so the check you type in a terminal is the same check that runs in cron
  • + It exposes the layer, not just the answer: SERVER, query time, EDNS buffer size, EDE codes and packet size tell you whether your resolver, the zone or the network path is at fault

cons

  • − Exit status is close to useless — its own manual says 0 means only 'DNS response received, including NXDOMAIN status' — so anything branching on $? is testing nothing
  • − The default output wraps one line of answer in a page of header metadata, which is exactly what beginners bounce off, while +short then hides the rcode and flags you need for real diagnosis
  • − The option surface has grown for 38 years and some combinations simply refuse: +trace cannot be combined with +short, and +tls/+https demand a server URL rather than an IP address

Takeaways

  1. 1Read the header before the answer: NOERROR, NXDOMAIN, SERVFAIL and REFUSED are four different problems owned by four different people.
  2. 2'flags: qr aa' with no 'rd' means an authoritative server answered from its own zone — that is how you separate a stale cache from a wrong record.
  3. 3Treat exit 0 as 'a DNS reply arrived', not 'the name resolved'; use [ "$(dig +short host)" ] as the actual test.
  4. 4Bound black holes with +time=1 +tries=1 — the default three tries per nameserver is what makes dig feel like it hung.
  5. 5Only @server, +short, +norecurse, +trace and +dnssec are load-bearing in daily work; the rest is archaeology.

Related commands

← all learning