Skip to content
Menu

Networking11 min read

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 domainsexample.com, google.com. The domain owner configures authoritative nameservers (often via a registrar or DNS provider like Cloudflare, Route 53, etc.).
  • Subdomainsapi.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:

  1. Application

    Calls getaddrinfo()

  2. Stub Resolver

    Checks local sources

  3. Recursive Resolver

    Does the heavy lifting

  4. Root Server

    Directs to TLD

  5. TLD Server

    Directs to auth NS

  6. 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:

  1. /etc/hosts — static name-to-IP mappings. Checked first (on most systems, per /etc/nsswitch.conf ordering: hosts: files dns).
  2. Local cache — if systemd-resolved or nscd is running, previously resolved names may be cached locally. macOS has mDNSResponder.
  3. Sends query to recursive resolver — configured in /etc/resolv.conf (the nameserver lines), or managed by systemd-resolved (which listens on 127.0.0.53 and forwards upstream).

Common /etc/resolv.conf entries

console
# 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:5

Warning

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.253 or 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):

console
bash

    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:

console
bash

    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:

console
bash

    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

Recursive Query

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
Iterative Query

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.
console
console

    $ 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.

console
console

    $ 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)

Transaction ID 16 bits — matches query to response
Flags 16 bits — QR, Opcode, AA, TC, RD, RA, RCODE
QDCOUNT 16 bits — # questions
ANCOUNT 16 bits — # answers
NSCOUNT 16 bits — # authority
ARCOUNT 16 bits — # additional

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).

bash
;; QUESTION SECTION:
;api.example.com.  IN  A

Answer Section

The actual resource records that answer the question. Empty in referral responses.

bash
;; ANSWER SECTION:
api.example.com. 300 IN A 93.184.216.34

Authority Section

NS records indicating which nameservers are authoritative for the zone. Used in referrals and alongside answers.

bash
;; 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.

bash
;; ADDITIONAL SECTION:
ns1.example.com. 172800 IN A 198.51.100.1

DNS 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.
UDP (Default)
  • 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
TCP (Fallback)
  • 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:

Application
Stub Resolver
Recursive Resolver
getaddrinfo("api.example.com")
Check /etc/hosts -> miss; query recursive
  1. App -> Stub resolver: getaddrinfo("api.example.com")
  2. Stub -> Recursive: "Resolve api.example.com A" (RD=1, recursive query)
  3. Recursive -> Root (198.41.0.4): "Resolve api.example.com A" (iterative) -> Gets referral to .com TLD servers
  4. Recursive -> .com TLD (192.5.6.30): "Resolve api.example.com A" (iterative) -> Gets referral to ns1.example.com + glue
  5. Recursive -> ns1.example.com (198.51.100.1): "Resolve api.example.com A" (iterative) -> Gets answer: 93.184.216.34, TTL=300
  6. Recursive caches answer (TTL=300): returns to stub resolver -> returns to application
  7. 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.

Solidnines — solidnines.com