How DNS Works
The complete resolution process — from query to answer
DNS: A Hierarchical, Distributed Database
DNS is not a single server or a flat lookup table. It is a globally distributed, hierarchical database designed to map human-readable domain names to IP addresses (and other data). The hierarchy mirrors the domain name structure itself: the root zone delegates to TLDs, TLDs delegate to authoritative nameservers, and so on.
The DNS Hierarchy
- Root zone (
.) — the top of the tree. Managed by IANA, served by 13 root server clusters (a.root-servers.net through m.root-servers.net). Each cluster is anycast across hundreds of physical servers worldwide. - TLD (Top-Level Domain) —
.com,.org,.io,.net, country codes like.uk,.de. Each TLD has its own set of nameservers operated by a registry (e.g., Verisign for.com). - Second-level domains —
example.com,google.com. The domain owner configures authoritative nameservers (often via a registrar or DNS provider like Cloudflare, Route 53, etc.). - Subdomains —
api.example.com,k8s.internal.example.com. Delegation can continue further via NS records.
Note
Key insight: No single DNS server knows the entire namespace. Each level knows only about the next level down (delegation). This is what makes DNS scalable to billions of records.
Think of DNS like a filesystem: . is the root, .com is a directory, example.com is a subdirectory, and records within it are files. Each "directory" is managed by a different authority.
Full Resolution Walk
When an application (say, curl https://api.example.com) needs to resolve a hostname, the following chain of events occurs:
Application
Calls getaddrinfo()
Stub Resolver
Checks local sources
Recursive Resolver
Does the heavy lifting
Root Server
Directs to TLD
TLD Server
Directs to auth NS
Authoritative NS
Returns the answer
Step 1: Application Calls getaddrinfo()
The application itself does not perform DNS resolution. It calls the C library function getaddrinfo() (or the older gethostbyname()), which hands off to the system's stub resolver. This is a blocking call in most languages (including Python's socket.getaddrinfo(), Go's net.LookupHost(), etc.).
Tip
Ops detail: In containers and pods, the stub resolver behavior is controlled by the /etc/resolv.conf inside the container. In Kubernetes, this is configured via the pod's dnsPolicy (default: ClusterFirst, which points to CoreDNS).
Step 2: Stub Resolver Checks Local Sources
The stub resolver is a lightweight library that does not perform recursion itself. It checks:
/etc/hosts— static name-to-IP mappings. Checked first (on most systems, per/etc/nsswitch.confordering:hosts: files dns).- Local cache — if
systemd-resolvedornscdis running, previously resolved names may be cached locally. macOS hasmDNSResponder. - Sends query to recursive resolver — configured in
/etc/resolv.conf(thenameserverlines), or managed bysystemd-resolved(which listens on127.0.0.53and forwards upstream).
Common /etc/resolv.conf entries
# Typical Linux server
nameserver 10.0.0.2
search internal.example.com example.com
options ndots:5 timeout:2 attempts:3
# Kubernetes pod (ClusterFirst dnsPolicy)
nameserver 10.96.0.10 # CoreDNS ClusterIP
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5Warning
K8s gotcha — ndots:5: With ndots:5, any name with fewer than 5 dots is treated as a relative name. So api.example.com (2 dots) first tries api.example.com.default.svc.cluster.local, then api.example.com.svc.cluster.local, then api.example.com.cluster.local, and finally api.example.com. (absolute). This means 4 NXDOMAIN responses before the real resolution — a common source of latency. Fix: append a trailing dot (api.example.com.) or reduce ndots.
Step 3: Query to Recursive Resolver
The stub resolver sends a DNS query to the recursive resolver (also called a "recursor" or "caching resolver"). This is the server that does the actual work of walking the DNS hierarchy. Common recursive resolvers:
- Cloud/ISP provided: AWS VPC resolver (
169.254.169.253or VPC base +2), Google Cloud metadata DNS, ISP resolvers - Public: Google (
8.8.8.8,8.8.4.4), Cloudflare (1.1.1.1), Quad9 (9.9.9.9) - On-prem: BIND, Unbound, PowerDNS Recursor, dnsmasq
- K8s: CoreDNS (handles cluster-internal names, forwards external queries upstream)
The recursive resolver first checks its own cache. If there's a cache hit with a valid TTL, it returns immediately. Otherwise, it begins the iterative resolution process.
Step 4: Recursive Resolver Queries Root Servers
The recursive resolver starts at the root. It sends an iterative query to one of the 13 root server addresses (which it knows from the built-in root hints file):
Query: What is the A record for api.example.com?
Root says: I don't know, but .com is handled by these TLD servers:
a.gtld-servers.net 192.5.6.30
b.gtld-servers.net 192.33.14.30
... (13 total)
Note
13 root server clusters: The addresses a.root-servers.net through m.root-servers.net are anycast — each IP is served by hundreds of physical servers worldwide. There are over 1,700 instances globally. The "13" is a limitation of fitting all addresses in a single 512-byte UDP response.
Step 5: Root Refers to TLD Server
The root server returns a referral (NS records + glue records in the additional section) pointing to the TLD servers for .com. The recursive resolver then queries the .com TLD server:
Query to .com TLD: What is the A record for api.example.com?
TLD says: I don't know, but example.com is handled by:
ns1.example.com 198.51.100.1
ns2.example.com 198.51.100.2
Step 6: TLD Refers to Authoritative Nameserver
The recursive resolver follows the referral and queries the authoritative nameserver for example.com:
Query to ns1.example.com: What is the A record for api.example.com?
Authoritative says: api.example.com A 93.184.216.34 (TTL 300)
The recursive resolver caches the answer (respecting the TTL), and returns it to the stub resolver, which returns it to the application.
Iterative vs Recursive Queries
Client to recursive resolver:
- "Give me the final answer — do whatever it takes"
- The resolver must return the answer or an error
- Used by stub resolvers when talking to their configured recursive resolver
- Flag:
RD=1(Recursion Desired) in query,RA=1(Recursion Available) in response
Recursive resolver to other DNS servers:
- "Give me the best answer you have — a referral is fine"
- The server can return a referral (NS records) instead of the final answer
- Used by recursive resolvers when walking the hierarchy
- The recursive resolver follows referrals itself, one hop at a time
Tip
In practice: Your application makes a recursive query to the recursive resolver. The recursive resolver then makes iterative queries to root -> TLD -> authoritative. The recursive resolver does all the work so the client doesn't have to.
Caching and TTL
Caching is fundamental to DNS performance. Without it, every single DNS lookup would require multiple round-trips to root, TLD, and authoritative servers.
How TTL Works
- Every DNS record has a TTL (Time to Live) value in seconds, set by the authoritative nameserver.
- When a recursive resolver caches a record, it decrements the TTL over time. When it reaches 0, the cached entry is evicted.
- The recursive resolver returns the remaining TTL to clients, not the original.
- Typical TTLs: 300s (5 min) for dynamic records, 3600s (1 hour) for stable records, 86400s (24 hours) for very static records.
| TTL Value | Use Case | Tradeoff |
|---|---|---|
60 (1 min) |
Records that change frequently (failover, blue/green deploys) | High query volume to authoritative servers |
300 (5 min) |
General-purpose web records | Good balance — most common default |
3600 (1 hour) |
Stable services, MX records | Changes propagate slowly |
86400 (24 hours) |
NS records, rarely-changing infrastructure | Very slow propagation; pre-lower TTL before changes |
Warning
Ops pattern — TTL pre-lowering: Before a planned DNS migration, lower the TTL well in advance (at least 2x the current TTL before the change). For example, if the current TTL is 3600, lower it to 60 at least 2 hours before the actual change. This ensures all caches have the short TTL by the time you make the cutover.
Negative Caching
NXDOMAIN (non-existent domain) responses are also cached. This prevents a flood of queries for names that don't exist.
How Negative Caching Works
- When a domain does not exist, the authoritative server returns
RCODE=NXDOMAIN. - The response includes the SOA record for the zone in the authority section.
- The SOA record's minimum TTL field (the last number in the SOA record) determines how long the negative answer is cached.
- Defined in RFC 2308.
$ dig nonexistent.example.com
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 45231
;; AUTHORITY SECTION:
example.com. 3600 IN SOA ns1.example.com. admin.example.com. (
2024010101 ; serial
7200 ; refresh
3600 ; retry
1209600 ; expire
300 ; minimum TTL -> negative cache TTL )
Note
K8s implication: With ndots:5, failed lookups for external names generate multiple NXDOMAIN responses that get negatively cached. This is why you may see unexpectedly high DNS query rates from pods — each external lookup can produce 4+ queries before reaching the correct FQDN.
Glue Records
Glue records solve a chicken-and-egg problem in DNS delegation.
The Problem
Suppose example.com delegates to ns1.example.com. To resolve www.example.com, you need to ask ns1.example.com. But to find the IP of ns1.example.com, you need to query the nameserver for example.com — which IS ns1.example.com. Circular dependency.
The Solution: Glue Records
The parent zone (the TLD, .com in this case) stores A/AAAA records for the nameservers alongside the NS delegation. These are "glue" records — they appear in the additional section of referral responses, breaking the circular dependency.
$ dig example.com NS @a.gtld-servers.net
;; AUTHORITY SECTION:
example.com. 172800 IN NS ns1.example.com.
example.com. 172800 IN NS ns2.example.com.
;; ADDITIONAL SECTION (glue records):
ns1.example.com. 172800 IN A 198.51.100.1
ns2.example.com. 172800 IN A 198.51.100.2
Note
When are glue records required? Only when the nameserver is within the zone it serves (e.g., ns1.example.com serving example.com). If you use external nameservers (e.g., ns1.cloudflare.com for example.com), no glue is needed — the resolver can resolve ns1.cloudflare.com independently via .com -> cloudflare.com.
DNS Message Format
DNS queries and responses share the same binary message format (defined in RFC 1035). Understanding the structure helps when reading packet captures or debugging with dig.
Message Header (12 bytes)
Header Flags Breakdown
| Flag | Bits | Meaning |
|---|---|---|
QR |
1 | 0 = query, 1 = response |
Opcode |
4 | 0 = standard query, 4 = notify, 5 = update |
AA |
1 | Authoritative Answer — response came from authoritative NS |
TC |
1 | Truncated — response was too large for UDP, retry over TCP |
RD |
1 | Recursion Desired — client wants full resolution |
RA |
1 | Recursion Available — server supports recursion |
RCODE |
4 | 0 = NOERROR, 2 = SERVFAIL, 3 = NXDOMAIN, 5 = REFUSED |
Message Sections
Question Section
What the client is asking: the domain name, query type (A, AAAA, MX, etc.), and class (almost always IN for Internet).
;; QUESTION SECTION:
;api.example.com. IN AAnswer Section
The actual resource records that answer the question. Empty in referral responses.
;; ANSWER SECTION:
api.example.com. 300 IN A 93.184.216.34Authority Section
NS records indicating which nameservers are authoritative for the zone. Used in referrals and alongside answers.
;; AUTHORITY SECTION:
example.com. 172800 IN NS ns1.example.com.Additional Section
Extra records that may be useful — typically A/AAAA records for nameservers listed in the authority section (glue records), or OPT records for EDNS0.
;; ADDITIONAL SECTION:
ns1.example.com. 172800 IN A 198.51.100.1DNS Transport: UDP, TCP, and EDNS0
Transport Rules
- UDP port 53 is the primary transport. Queries and responses are typically a single datagram.
- Original limit: 512 bytes maximum for UDP DNS messages (RFC 1035).
- TCP port 53 is used when a response is truncated (TC flag set), or for zone transfers (AXFR/IXFR).
- EDNS0 (Extension Mechanisms for DNS, RFC 6891) extends the UDP payload size — typically to 1232 or 4096 bytes — via an OPT pseudo-record in the additional section.
- Single packet, no connection setup
- Fast — no handshake overhead
- 512 bytes (classic) or up to ~4096 with EDNS0
- Susceptible to spoofing (no connection state)
- Used for ~99% of DNS queries
- Full TCP handshake (3-way) before query
- Slower — but no size limit
- Required for zone transfers (AXFR/IXFR)
- Used when UDP response is truncated (TC=1)
- Also used for DNS over TLS (DoT, port 853)
Tip
Firewall rule reminder: DNS requires both UDP and TCP on port 53. Blocking TCP/53 will cause failures for large responses (DNSSEC-signed responses are often >512 bytes) and zone transfers. This is a common misconfiguration in security groups and network policies.
Putting It All Together
Here is a complete sequence diagram showing the resolution of api.example.com from an application's perspective, assuming a completely cold cache:
- App -> Stub resolver:
getaddrinfo("api.example.com") - Stub -> Recursive: "Resolve
api.example.com A" (RD=1, recursive query) - Recursive -> Root (198.41.0.4): "Resolve
api.example.com A" (iterative) -> Gets referral to.comTLD servers - Recursive -> .com TLD (192.5.6.30): "Resolve
api.example.com A" (iterative) -> Gets referral tons1.example.com+ glue - Recursive -> ns1.example.com (198.51.100.1): "Resolve
api.example.com A" (iterative) -> Gets answer:93.184.216.34, TTL=300 - Recursive caches answer (TTL=300): returns to stub resolver -> returns to application
- Subsequent queries within 300s: Recursive resolver returns from cache immediately (single round-trip instead of 4)
Note
Latency in practice: A cold-cache resolution requires 4 round-trips (stub -> recursive, recursive -> root, -> TLD, -> authoritative). With caching, subsequent lookups are a single round-trip (stub -> recursive). This is why the first request to a new domain is noticeably slower.