TRT
Sign in
Networking

DNS Resolution End to End: From resolv.conf to Recursive Lookup

What really happens when you resolve a hostname — resolvers, cache TTLs, CNAME chains, EDNS, TCP fallback on truncation, and how DoH changes the path.

The Runtime Theory Team8 min read#dns#resolvers#caching#doh#networking

Most engineers treat DNS as a magic function: give it a name, get an IP. In reality, the name never gets "looked up" — it gets asked about through a chain of independent systems, each with its own cache, its own timeout, and its own failure modes. This is the whole path, step by step, from your process to the root servers.

Step 1: the resolver configuration

Before anything else, the kernel's resolver needs to know who to ask. On Linux that's /etc/resolv.conf:

bash
nameserver 127.0.0.53     # systemd-resolved stub, forwards upstream
nameserver 8.8.8.8        # fallback, used only if the first fails
options timeout:2 attempts:2 ndots:1

Two details matter here. First, ndots:1 means a name with at least one dot (like api.theruntimetheory.com) is treated as fully qualified and queried as-is; a bare hostname like db is tried against the search domains first. Second, timeout:2 attempts:2 means each upstream gets 2 seconds, twice, before the resolver gives up and returns an error to your application. A slow DNS server shows up in your request latency as a hard, fixed number — not a gradual slowdown.

Step 2: caches are where most lookups end

The vast majority of DNS queries never leave the machine. glibc's nscd or systemd-resolved keeps a local cache keyed by (name, type) with entries that expire according to the record's TTL — the number of seconds the authoritative server declared the answer valid. The cache is a performance layer, but it's also a consistency layer: as long as the TTL hasn't expired, every process on the machine sees the same IP, even if the DNS zone changed upstream five seconds ago.

Step 3: the recursive walk

If the local cache misses, the resolver performs a recursive resolution. Every resolver (starting from the stub on your machine, through your ISP's or Cloudflare's recursor) does the same walk:

text
client -> resolver: "what is api.theruntimetheory.com?"
resolver -> root server: "who serves .com?"
resolver -> .com TLD server: "who serves theruntimetheory.com?"
resolver -> theruntimetheory.com authoritative server:
             "what is api.theruntimetheory.com?"
authoritative -> resolver: A 104.21.62.13  (TTL 300)
resolver -> client: A 104.21.62.13

Each answer is a referral to the next zone, and the resolver caches every step of the walk. That's why a root server outage is survivable in practice: the root zone is cached for days, and only the TLD step needs refreshing.

Step 4: CNAME chains are extra lookups, not redirects

A CNAME record is an alias: it tells the resolver "the real name is X, keep going." This is how every CDN works:

bash
$ dig +short blog.theruntimetheory.com
blog.theruntimetheory.com.cdn.cloudflare.net.   # CNAME
104.21.62.13                                    # A record, different zone

Critically, the client sees only the final A record — the extra lookup happens entirely inside the resolver, and each hop in the chain is cached separately with its own TTL. A CNAME pointing at a CDN with a 60-second TTL means your edge DNS is effectively uncached: every miss walks the chain again.

Step 5: EDNS and the TCP fallback

Almost all queries now carry EDNS(0), which negotiates a larger UDP payload (up to 4096 bytes) so answers fit in a single packet. But large answers still exceed UDP limits, and middleboxes still mangle big UDP packets. The protocol's escape hatch: the server responds with a truncated answer — the TC bit set — and the resolver is required to retry the query over TCP, where answers can be as big as needed.

bash
$ dig +ignore +tcp example.com AAAA   # force the TCP path

You can trigger this yourself with a large TXT record: the first response arrives with truncated: 1 and the resolver silently retries on TCP. When you see "timeout resolving" errors, it's often this fallback failing — the UDP query worked, but the TCP retry hit a firewall that blocks port 53/TCP.

Step 6: the protocol change — DoH and DoT

Classic DNS sends queries in clear UDP to port 53 — visible to your ISP, your Wi-Fi operator, and any middlebox. DoH (DNS over HTTPS) wraps the same query in HTTPS to port 443; DoT uses TLS on 853. The resolver path stays identical; only the last hop (client → resolver) becomes encrypted. The trade-off: DoH hides the query from the network, but hands it to a single resolver (your chosen DoH provider), which sees everything.

The mental model

  • resolv.conf picks the resolver; TTLs decide how often it's actually consulted.
  • CNAMEs are resolution-time indirection, invisible to the application.
  • Truncation + TCP is the protocol's designed fallback, and the most common silent DNS failure.
  • Every hop — stub, recursor, TLD, authoritative — is a separate cache with its own staleness window. "Propagation" is not magic; it's a countdown of TTLs.

Production cost model and latency numbers

To apply DNS Resolution End to End: From resolv.conf to Recursive Lookup effectively in production systems, engineers must move beyond qualitative diagrams and quantify the exact hardware and network costs at each boundary:

Operation / BoundaryTypical LatencyDominant BottleneckProduction Mitigation
L1 / L2 Cache Access1 ns – 4 nsRegister dependency chains & pointer chasingContiguous memory layouts, cache-line alignment (64 B), and batch iteration
Main Memory (DRAM) Fetch60 ns – 100 nsTLB misses, random heap indirection, NUMA hopsHugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers
Kernel Syscall / Context Switch1 µs – 5 µsUser-to-kernel mode switch, page-table flush, lock contentionNon-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O
NVMe SSD Random Read / fsync50 µs – 500 µsBlock I/O queue depth, write amplification, WAL flushesGroup commit, sequential append logs, and bounded buffer-pool pinning
Intra-Region RPC Roundtrip0.5 ms – 2.5 msSerialization, TCP/TLS framing, tail queueing ($L = \lambda W$)Connection pooling, deadline propagation, and p95 hedged requests

Failure modes, debugging, and operational checklist

In real incidents, systems rarely fail at average (p50) load—they fail when a slow dependency, skewed hot key, or resource leak pushes concurrency across a non-linear saturation cliff. When reviewing code or architecture that relies on DNS Resolution End to End: From resolv.conf to Recursive Lookup, walk through three verification questions:

  1. What is the invariant under partial failure or contention? If a worker crashes mid-operation, a network packet is delayed and reordered, or two threads race on the same state transition, prove which atomic primitive, version number, or idempotency key prevents silent corruption.
  2. How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized 100 ms slowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early.
  3. Which metrics and traces isolate the bottleneck in under 60 seconds? Instrument saturation, queue wait time vs service execution time, cache hit/miss ratios, and error rates by failure reason so on-call engineers can immediately distinguish a compute bottleneck from an I/O or lock-contention stall.

Spotted a mistake or something broken? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save