Before a single TCP connection can open, the machine has to turn example.com into an IP address. DNS looks like a single lookup, but it is a small distributed system running on your behalf. Here is the literal sequence.
The application calls getaddrinfo("example.com", "443", …) — every higher-level API (curl, socket.connect in Node, InetAddress in Java) bottoms out here. This is a libc function, so the call stays entirely in user space for now: no syscall has happened yet.
libc consults /etc/nsswitch.conf, which orders the name service sources. On a typical Linux box that is files dns: check /etc/hosts first. A manually entered hostname returns immediately — sub-microsecond, zero packets. Only on a miss does the resolver path start.
The stub resolver (libc's own, or systemd-resolved via NSS) checks its in-process cache. If a previous answer for this name is alive within its TTL, it returns the cached address and the whole trace stops here — typically 50–200 µs of user-space work with no network I/O.
Cache miss. The stub builds a query: a 12-byte header plus a question section (example.com, type A, class IN), then sends it as a single UDP datagram to the configured recursive resolver — commonly a local resolver on 127.0.0.53 or the router at 192.168.1.1, port 53. One sendto, one recvfrom. Because UDP has no handshake, the entire query is one round trip on the LAN: ~0.3–1 ms.
The recursive resolver walks the delegation chain. It asks a root server for com (a root hint already cached, anycasted globally), gets the com TLD server list, asks a TLD server for example.com, and finally asks the authoritative server that owns the zone. Each hop is a fresh UDP exchange, 5–30 ms each on a typical WAN. This is the expensive path — one cold lookup can take 50–150 ms.
The answer — example.com → 93.184.216.34 with TTL 3600 — comes back as a response packet. The stub stores it in its cache keyed by name and record type, and returns to the application. Subsequent lookups inside the TTL window never leave the machine.
strace -e trace=network,read,write -f curl -s https://example.com 2>&1 | grep -E "socket|sendto|recvfrom"The layers involved read like a ladder: application → libc (user space) → resolver cache (user space) → kernel UDP socket → LAN → recursive server → internet. Every stage after step 3 is network-bound, and every stage before it is memory-bound. That asymmetry is why DNS performance is cache performance.