DNS Cheatsheet

How Resolution Works

Use this DNS reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

The four players

RoleWhat it isExample
Stub resolverThe library in your process that asks a question and caches nothing muchgetaddrinfo, systemd-resolved, Go's resolver
Recursive resolverDoes the walking, caches aggressively8.8.8.8, 1.1.1.1, your ISP, a local unbound
Root serversKnow only who runs each TLDa.root-servers.net through m
Authoritative serversHold the real zone data for a domainns1.example.com

The recursive resolver does all the work. Your application talks to exactly one box and waits.

The walk, step by step

Resolving www.example.com A from a cold cache:

1. The application calls getaddrinfo("www.example.com"). The stub resolver reads /etc/nsswitch.conf and /etc/hosts first, then falls through to DNS. 2. The stub sends one UDP query to the first nameserver in /etc/resolv.conf, with the RD (recursion desired) bit set. 3. The recursive resolver checks its cache. On a hit it answers immediately and the walk stops here. This is the normal case in production. 4. On a miss, it asks a root server for www.example.com A. The root does not know, and does not recurse. It returns a referral: NS records for com. plus their glue A records, with the AA bit clear and an empty answer section. 5. It asks a com. TLD server the same question. Another referral: NS records for example.com. plus glue. 6. It asks an authoritative server for example.com.. This one answers with the AA (authoritative answer) bit set and the A record in the answer section. 7. The resolver caches the answer for its TTL, plus every referral it collected along the way, and returns the answer to the stub with RA (recursion available) set. 8. The stub hands the address list to the application. Nothing above the resolver knows how many round trips it took.

Steps 4 and 5 almost never happen in practice. Root and TLD referrals have TTLs measured in days, so a busy resolver holds them permanently and a cold lookup is usually one query to the authoritative server.

Watch it happen

dig +trace performs the walk itself, iteratively, so you see each referral.

dig +trace www.example.com A
dig +trace www.example.com A | grep -E '^\S+\s+\d+\s+IN\s+(NS|A)'
.                 454818 IN NS a.root-servers.net.
;; Received 811 bytes from 192.168.1.1#53(192.168.1.1) in 12 ms

com.              172800 IN NS a.gtld-servers.net.
;; Received 1170 bytes from 198.41.0.4#53(a.root-servers.net) in 24 ms

example.com.      172800 IN NS a.iana-servers.net.
;; Received 771 bytes from 192.5.6.30#53(a.gtld-servers.net) in 31 ms

www.example.com.  300 IN A 93.184.215.14
;; Received 60 bytes from 199.43.135.53#53(a.iana-servers.net) in 18 ms

Each ;; Received ... from line is one hop. Where the output stops is where the delegation breaks.

The flags that tell you what you got

FlagNameMeaning
qrQuery responseThis is a response, not a question
rdRecursion desiredThe client asked for recursion
raRecursion availableThe server will recurse for you
aaAuthoritative answerStraight from the zone, not a cache
tcTruncatedToo big for UDP, retry over TCP
adAuthenticated dataDNSSEC validated by the resolver
cdChecking disabledClient asked to skip DNSSEC validation
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 41005
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

aa present means you queried the authoritative server directly. aa absent with an answer means you got a cached copy.

Response codes

RCODENameMeans
0NOERRORQuery succeeded. An empty answer section means NODATA, the name exists but has no record of that type
1FORMERRThe server could not parse the query
2SERVFAILServer failed, often a DNSSEC validation failure or a dead authoritative server
3NXDOMAINThe name does not exist, at all, for any type
4NOTIMPOpcode not implemented
5REFUSEDServer will not answer you, usually an ACL or a closed recursor
9NOTAUTHNot authoritative for that zone

NOERROR with zero answers is not the same as NXDOMAIN. NODATA means "ask for a different type". NXDOMAIN means the whole name is absent, and it is cached against every type at once.

Transport, ports, and size

- UDP port 53 is the default. One datagram out, one back. - Plain DNS caps at 512 bytes without EDNS(0). With EDNS(0) the client advertises a larger buffer, commonly 1232 bytes to stay under typical MTU. - Truncation sets tc and the client retries over TCP port 53. A firewall that allows UDP 53 but blocks TCP 53 breaks large answers, DNSSEC, and zone transfers. - AXFR is always TCP, never UDP. - Encrypted transports: DoT on TCP 853, DoH on HTTPS 443 at /dns-query, DoQ on QUIC 853. All three protect the stub-to-resolver hop only, not the resolver's own walk.

dig example.com A +tcp                # force TCP
dig example.com TXT +bufsize=4096     # bigger EDNS buffer
dig example.com A +noedns             # what a pre-1999 client sees

Gotchas

- The stub checks /etc/hosts first. A stale entry there beats every DNS record and no dig will ever show it, because dig skips /etc/hosts entirely. Use getent hosts name to see what the system actually resolves. - search domains in /etc/resolv.conf rewrite short names. ping api can become api.corp.example.com. ndots controls the threshold, and it is the cause of most surprising Kubernetes lookups. - A resolver caches negative answers too, so a typo you just fixed can stay broken for the SOA minimum TTL. - dig bypasses the system stub completely. It builds packets itself, so dig working while your app fails points at nsswitch.conf, /etc/hosts, or a container's resolver, not at DNS. - Anycast means 8.8.8.8 is hundreds of machines with independent caches. Two queries in a row can hit different caches and disagree.