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
| Role | What it is | Example |
|---|---|---|
| Stub resolver | The library in your process that asks a question and caches nothing much | getaddrinfo, systemd-resolved, Go's resolver |
| Recursive resolver | Does the walking, caches aggressively | 8.8.8.8, 1.1.1.1, your ISP, a local unbound |
| Root servers | Know only who runs each TLD | a.root-servers.net through m |
| Authoritative servers | Hold the real zone data for a domain | ns1.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
| Flag | Name | Meaning |
|---|---|---|
qr | Query response | This is a response, not a question |
rd | Recursion desired | The client asked for recursion |
ra | Recursion available | The server will recurse for you |
aa | Authoritative answer | Straight from the zone, not a cache |
tc | Truncated | Too big for UDP, retry over TCP |
ad | Authenticated data | DNSSEC validated by the resolver |
cd | Checking disabled | Client 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
| RCODE | Name | Means |
|---|---|---|
| 0 | NOERROR | Query succeeded. An empty answer section means NODATA, the name exists but has no record of that type |
| 1 | FORMERR | The server could not parse the query |
| 2 | SERVFAIL | Server failed, often a DNSSEC validation failure or a dead authoritative server |
| 3 | NXDOMAIN | The name does not exist, at all, for any type |
| 4 | NOTIMP | Opcode not implemented |
| 5 | REFUSED | Server will not answer you, usually an ACL or a closed recursor |
| 9 | NOTAUTH | Not 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.