20. DNS: resolution, records, caching, and modern transports
DNS turns names into addresses (and much more). It is a distributed, cached, hierarchical database. Almost every "the internet is slow/broken" incident has a DNS chapter.
20.1 The name hierarchy
. <- root
+-- com. <- TLD (top-level domain)
+-- example.com. <- second-level (a "zone" you control)
+-- www.example.com. <- a record within the zone
+-- api.example.com.
The trailing dot = the root; www.example.com and www.example.com. are the
same. Each zone is authoritatively served by a set of name servers and can
delegate sub-zones (e.g. example.com delegates eng.example.com to another
team's servers via NS records + glue).
20.2 The players
| Role | What it does |
|---|---|
| Stub resolver | The tiny client in your OS/app (getaddrinfo). Asks one recursive resolver, caches little or nothing. |
| Recursive resolver (a.k.a. caching resolver) | Does the legwork: root → TLD → authoritative, caches every answer by TTL, returns the final answer. Examples: your ISP's, 1.1.1.1, 8.8.8.8, a corporate unbound/bind. |
| Authoritative name server | Holds the actual zone data for domains it's responsible for. Never recurses. Examples: Route 53, NS1, Cloudflare, your own bind. |
| Forwarder | A resolver that just relays to another resolver (home routers do this). |
20.3 A full recursive resolution (cold cache)
Client wants A for shop.example.com:
stub -> recursive: "A shop.example.com?" (recursion desired)
recursive -> a ROOT server: "A shop.example.com?"
root -> recursive: "I don't know, but ask .com servers: NS a.gtld-servers.net (+ glue A/AAAA)"
recursive -> a .COM server: "A shop.example.com?"
.com -> recursive: "ask example.com's servers: NS ns1.example.com (+ glue)"
recursive -> ns1.example.com: "A shop.example.com?"
ns1 -> recursive: "A shop.example.com = 93.184.216.34, TTL 300" (authoritative, AA bit set)
recursive caches every referral + the final answer, then:
recursive -> stub: "93.184.216.34, TTL 300"
Next lookup of anything in .com skips the root; next lookup in example.com
skips .com; next lookup of shop.example.com within 300 s is served straight
from cache. This is why the first request to a new domain is slow and the rest
are instant.
20.4 Record types you must know
| Type | Purpose | Example |
|---|---|---|
| A | name → IPv4 | example.com. 300 IN A 93.184.216.34 |
| AAAA | name → IPv6 | example.com. 300 IN AAAA 2606:2800:220:1:... |
| CNAME | alias → canonical name | www.example.com. IN CNAME example.com. — cannot coexist with other records at the same name, so never at the zone apex |
| MX | mail servers + priority | example.com. IN MX 10 mail.example.com. |
| NS | delegation: who's authoritative | example.com. IN NS ns1.example.com. |
| SOA | zone metadata: primary NS, admin email, serial, refresh/retry/expire, minimum (negative-cache) TTL | one per zone |
| TXT | arbitrary text | SPF, DKIM, domain-verification tokens |
| PTR | IP → name (reverse DNS) | in in-addr.arpa / ip6.arpa; used by mail servers, logging |
| SRV | service location: host + port + priority + weight | _sip._tcp.example.com. IN SRV 10 60 5060 sipserver... |
| CAA | which CAs may issue certs for this domain | issuance policy |
| DS / DNSKEY / RRSIG / NSEC(3) | DNSSEC chain of trust | signatures + delegation-signer + authenticated denial |
| SVCB / HTTPS | service binding: ALPN, port, IP hints, ECH config — lets a client learn "use HTTP/3 on port 443 at these IPs" in one lookup | example.com. IN HTTPS 1 . alpn=h3,h2 ipv4hint=... |
| CNAME at apex workarounds | ALIAS/ANAME/CNAME-flattening |
provider-specific |
20.5 TTL and caching — the operational core
- Every record has a TTL; resolvers must not serve it longer than that.
- Lowering TTL takes effect only after the old TTL expires. To migrate an IP with minimal downtime: drop the TTL to 60 s at least the old-TTL duration in advance, then change the record, then raise the TTL back afterward.
- Negative caching: "no such name" (NXDOMAIN) and "no such record" are cached
too, for the SOA
minimumfield's duration. A typo'd record that returns NXDOMAIN can haunt you for hours. - Resolvers and OSes and browsers each cache.
digbypasses the OS/browser cache; your app may not. - Some resolvers clamp absurd TTLs (cap at a day, floor at a few seconds) and prefetch popular records before expiry.
20.6 Transport: UDP first, TCP fallback, then encrypted
- Classic DNS: UDP/53, one query per packet. If the response is too big for the client's advertised EDNS0 buffer (or > 512 bytes without EDNS), the server sets the TC (truncated) bit and the client retries over TCP/53. You must allow TCP/53 — DNSSEC, large TXT, and many CDNs need it.
- EDNS0: an OPT pseudo-record that advertises a larger UDP buffer (commonly 1232 today, to dodge fragmentation — Ch. 15), plus flags and options (ECS = EDNS Client Subnet, which passes a truncated client prefix to authoritative servers for geo-accurate answers).
- DoT (DNS over TLS): port 853. DNS inside TLS. Easy to identify/block by port.
- DoH (DNS over HTTPS): port 443, looks like normal web traffic. Used by browsers (often bypassing your OS resolver and your network's DNS policy — a real operational headache: your split-horizon/internal names stop resolving).
- DoQ (DNS over QUIC): port 853/UDP, no head-of-line blocking.
- DNSSEC ≠ encryption. It's authentication/integrity: authoritative servers
sign records (
RRSIG), a chain ofDS→DNSKEYfrom the root lets a validating resolver detect tampering. It does not hide queries.
20.7 Split-horizon / split-brain DNS
The same name resolves differently depending on who asks: internal clients get
10.x (private), external clients get the public IP. Implemented with views
(BIND), separate resolvers, or "internal" zones. Breaks when: a laptop uses DoH
and bypasses the internal resolver; a VPN doesn't push the internal search
domain; caching carries an internal answer to the coffee shop.
20.8 Resolution details that bite
searchdomains +ndots:resolv.confsearch corp.example.comandoptions ndots:5meandbbecomesdb.corp.example.com, and evenfoo.bar.baz.qux(4 dots < 5) gets search suffixes tried first. In Kubernetes, defaultndots:5makes every external lookup do 4–5 failed queries first — a well-known latency/QPS amplifier. Fix:dnsConfigwithndots:1or use FQDNs with a trailing dot./etc/hostsand NSS order (/etc/nsswitch.conf:hosts: files dns) win before DNS.getaddrinfovsgethostbyname: the former is dual-stack and applies RFC 6724 sorting; use it.- Happy Eyeballs (Ch. 18) races the A/AAAA connects, not just the lookups.
- One resolver dead in
resolv.conf: default behavior tries them in order with a ~5 s timeout each → every lookup stalls 5 s.options rotate/timeout:1/attempts:2help; better, run a local caching resolver.
20.9 War story
A deploy "broke DNS" for a service mesh: p50 request latency jumped 150 ms.
tcpdump on port 53 showed five queries per outbound call —
svc, svc.ns.svc.cluster.local, svc.svc.cluster.local,
svc.cluster.local, svc.example.com — the first four NXDOMAIN. Root cause:
code used a bare short name with the default Kubernetes ndots:5. Fix: append
the trailing dot to make it a FQDN (svc.ns.svc.cluster.local.), cutting it to
one query. Lesson: ndots + search can multiply your DNS load 5×
invisibly; always check with a capture.
Try it yourself
dig +trace example.com # watch the full root->TLD->auth walk
dig +norecurse @a.root-servers.net example.com # ask a root server directly
dig example.com A +short
dig example.com HTTPS # SVCB/HTTPS record
dig -x 93.184.216.34 # reverse (PTR)
dig +dnssec example.com # see RRSIG; 'ad' flag = validated
dig @1.1.1.1 example.com # bypass local resolver
resolvectl status # systemd-resolved: per-link servers, DNSSEC, DoT
grep -E 'search|nameserver|options' /etc/resolv.conf
# Measure resolver latency:
for i in $(seq 5); do dig +noall +stats example$i.test @1.1.1.1 | grep 'Query time'; done