THE INTERVIEW QUESTION, ANSWERED PROPERLY
…and press ⏎ Enter ?
Eight interactive chapters, every layer of the stack. Each scene plays itself — grab ◀ ▶ to step through at your own pace. Dotted terms are tappable.
Before a single packet exists, the OS has to find out a key went down. Scroll to ride one from switch contact to the browser’s event loop — and watch where the five milliseconds actually go.
Your Enter key isn’t wired to the CPU. It shorts one intersection of a row×column grid that the keyboard’s own microcontroller sweeps about a thousand times a second. And because metal contacts physically bounce, the firmware then spends 2–5 ms just deciding whether you really pressed it. The slowest thing in this entire chapter is a spring settling.
The keyboard can’t speak until spoken to — USB is polled. An 8-byte report (modifier bits plus six key slots) waits on the interrupt IN endpoint until the host asks, up to 1 ms later — 125 µs on 8 kHz gaming boards. And all it says is 0x28: “the key at this position went down.” Which character that is? Not the keyboard’s problem.
The host controller raises an and the CPU drops whatever it was doing — between two instructions, no questions asked — to run the kernel’s interrupt handler. It grabs the report, defers the real work to a softirq, and returns in single-digit microseconds, because while it runs, that core does nothing else. You’ve spent years blaming “the computer” for input lag; the kernel’s share is a rounding error.
Now the kernel translates 0x28 into KEY_ENTER using a keymap that lives in the OS — the keyboard has no idea what its own keys mean. That one lookup table is how a single physical board serves QWERTY, AZERTY and Dvorak. An event record appears on /dev/input/eventN; on Windows, win32k queues a message instead.
One machine, dozens of windows, one keypress. The compositor checks a single piece of state — who holds keyboard focus — and delivers the event to the browser’s UI process alone. Your terminal and your chat app never learn that Enter was pressed. Wayland, macOS and Windows disagree on almost everything, except that this is their job.
The browser’s main thread has been parked in epoll_wait() (GetMessage on Windows), burning exactly 0% CPU — it doesn’t poll, it gets woken. The event makes a file descriptor readable, the scheduler wakes the thread, keydown fires, and Enter means: stop autocompleting, commit the omnibox. Elapsed: about five milliseconds. Packets sent: zero.
epoll_wait(2) (or GetMessage) — it burns zero CPU while idle. The input event makes the fd readable, the scheduler wakes the thread, and it reads an input record. Latency from contact closure to userspace is dominated by debounce (~2–5 ms) and USB polling (~0.5 ms avg), not the kernel, which handles the in single-digit microseconds. The report format is why cheap keyboards can only report 6 keys at once.The keydown landed; still zero packets. Before the network is allowed to exist, the omnibox runs a gauntlet of in-memory checks — watch one string earn its scheme, its slash, and its ticket to the wire.
The omnibox is two different programs wearing one text field, and your string has to be sorted before anything else can happen. A heuristic scores it like a hostname: contains a dot, no whitespace, and the suffix is on the public-suffix list. google.com passes all three — verdict: navigate. No packet was consulted; the whole trial runs on rules shipped with the browser.
was born in 1983; Unicode in 1991. So every hostname must be reduced to ASCII, and Unicode ones get -encoded to an xn-- form. The same machinery is a phishing defense: a Cyrillic а in аpple.com looks identical to yours, so mixed-script lookalikes are displayed as raw punycode instead of the pretty lie. Our string is plain ASCII and walks through.
You typed no scheme. The polite 1990s answer was to try http:// first and let the server upgrade you — one plaintext hop an attacker could sit on. Instead the browser consults the preload list compiled into its own binary: google.com is on it, so the scheme becomes https:// before the network even exists. The classic SSL-strip attack dies right here, killed by a lookup table.
The parser now normalizes ruthlessly: scheme and host to lowercase, the default :443 made implicit, an empty path rewritten to /. Pedantic? Everything after this looks the URL up by exact bytes — the HTTP cache, the connection pool, the service-worker scope. One canonical spelling is what makes GoOgLe.CoM and google.com the same website instead of two cache entries.
Before paying for the network, the browser checks what it already has: the in-memory cache (microseconds), the disk cache, and any service worker registered for this scope. Google’s homepage ships Cache-Control: private, max-age=0, so today is a miss on all three. It’s not stinginess — a fresh hit here would have ended the story five chapters early.
The honest footnote: Chrome-class browsers didn’t wait for your Enter. Around goog… the omnibox ranked this navigation as likely and already started speculative DNS — often a full + pre-connect. By the time your keypress finished chapter 00, the next three chapters may have already run. We’ll walk them anyway, because somebody has to.
Chapter 01 ended with a canonical name; the network routes on numbers. Zero packets have been spent in this story so far — that changes here. Watch one UDP question climb the only globally distributed database that actually works, and come back down as an address.
Before a single datagram is spent, four layers get to answer from memory: the browser’s own DNS cache, the OS stub resolver, the hand-edited /etc/hosts — a relic of 1970s ARPANET that still outranks everything — and your router’s cache. Any hit ends this chapter at roughly zero milliseconds. Assume today is the rare all-cold miss, on purpose: the story needs the walk.
The first packet of this entire story: a ~40-byte UDP datagram to port 53. A 12-byte header, then the name — not as text but as length-prefixed labels, 06 google 03 com 00. The dots you typed never go on the wire. The query ID and source port are randomized, which since the 2008 Kaminsky attack is most of the defense against strangers forging your answer. An AAAA (IPv6) twin launches in parallel.
Your resolver — 8.8.8.8, or whatever your ISP runs — starts at the top: the root zone, served by thirteen named letters, a–m.root-servers.net. Thirteen names, about 1,900 physical instances, so “the root” you reach is probably a rack in your own city. It has no idea where google.com is, and that’s fine: it returns NS records for the .com zone, and the resolver caches that referral for about two days.
Next stop: the .com gTLD servers, operated by Verisign — anycast clusters absorbing trillions of queries a day. Another referral: google.com is served by ns1–ns4.google.com. Notice the trap — the nameserver for google.com is itself a name inside google.com. So the response bundles glue records, the nameservers’ IP addresses included up front, because otherwise you’d need DNS to bootstrap DNS, forever.
ns1.google.com finally answers: google.com 300 IN A 142.250.72.14. But that address wasn’t looked up so much as chosen — Google’s authoritative servers pick an IP close to your resolver. The DNS answer is itself the first act of traffic engineering, performed before your machine has sent google.com a single byte.
The answer rides home and is stamped into every cache it passes — resolver, OS, browser — each honoring the TTL of 300 seconds. Google keeps it short on purpose: five minutes is as long as they’ll tolerate losing control of where you’re routed. Warm resolver: 1–20 ms. True cold walk: up to ~120 ms. Honest footnote: your browser may have skipped the OS entirely via DNS-over-HTTPS — and thanks to chapter 01’s speculative pre-flight, all of this likely finished while you were still typing.
RD=1 — “recursion desired,” the stub politely asking the resolver to do the whole walk you just watched. Answers over ~1,232 bytes set TC=1 and force a retry over TCP: DNS’s dirty secret is that it has always spoken TCP too. The A and AAAA answers feed , which races an IPv6 connection against IPv4 and keeps whichever wins. DNS-over-HTTPS moves the whole conversation into an encrypted session with the resolver, hiding your lookups from the coffee-shop Wi-Fi. And the answer stays -aware either way: Google hands you an IP near your resolver, not near you — which is why a distant VPN can make every site slower without any obvious cause.We have an address; now we need a conversation. TCP builds one out of three segments and two random numbers — watch two machines that have never met agree on shared state, for the price of exactly one round trip.
The network process calls socket(), then connect() toward 142.250.72.14:443. The kernel picks an ephemeral source port from a range you can read in /proc, and from here on the connection is its 4-tuple — your IP, that port, Google’s IP, 443. Nothing exists on the wire yet, no bandwidth is reserved, no path is chosen. “A connection” is an agreement two machines haven’t made yet.
The SYN is an opening bid, and its options clause decides performance for the connection’s entire life. MSS 1460 (the biggest segment that fits), SACK permitted (retransmit only what was actually lost), window scale 7 (receive windows up to 8 MB instead of 64 KB), timestamps (a running clock). There is no renegotiation — miss an option here and your only recourse is a new connection. It also carries seq=x, a random 32-bit .
The SYN-ACK acknowledges x+1 and bids the server’s own random ISN, y. The randomness is load-bearing — predictable ISNs once let off-path attackers inject data into strangers’ connections. And under a SYN flood the server stops keeping state at all: encode the whole half-open connection cryptographically inside the ISN, allocating memory only when your final ACK proves you are a real peer and not a forged address.
Your ACK (ack=y+1) completes the exchange, and both kernels flip to ESTABLISHED. Strip the ceremony away and that is the whole product: two machines that now agree on two 32-bit counters, from which every byte will be numbered, acknowledged, and retransmitted. Total price: exactly one round trip — and the ACK doesn’t even travel alone, because the TLS ClientHello is already glued behind it in the same flight.
How fast may we send? The network doesn’t publish capacity, so TCP probes for it. The congestion window () starts at ~10 segments — about 14.6 KB (RFC 6928) — and doubles every round trip until packet loss (CUBIC) or measured bandwidth (BBR) says stop. Fresh connections are slow on purpose; this is why CDNs keep connections warm, and why the first screen of a website ideally fits in the initial window.
The honest footnote: to Google, Chrome usually speaks — — where the transport handshake and the crypto handshake are the same packets, so this chapter and the next fold into one round trip and, on a resumed session, into zero. TCP is the first-ever-visit and fallback path. We walked it anyway: on the open web it still carries most of everything.
connect() hang for exactly 1, then 3, then 7 seconds. Once established, timestamps feed the RTT estimator continuously, and PAWS uses them to reject wrapped sequence numbers — at gigabit rates the 32-bit space wraps in about 17 seconds. SACK ranges let the sender repair exactly the hole instead of resending everything after it. And TCP Fast Open — data riding on the SYN, authorized by a cookie from a previous visit — works perfectly in the lab, but middleboxes mangled it often enough that browsers largely gave up on it.The TCP pipe is open, but it’s a postcard — every router on the path can read it. One more round trip buys total darkness: watch two strangers agree on a secret in public, prove who they are, and turn off the lights for everyone else.
TLS 1.3 needs exactly one round trip because the client refuses to negotiate politely: it guesses the key-exchange group — , because everyone picks X25519 — and ships its 32-byte public key share inside the very first message, along with supported versions, cipher suites, , and . TLS 1.2 spent a whole round trip deciding how to agree before agreeing; 1.3 presumes, and is almost never wrong.
One field in that hello has to stay readable: the SNI, naming the host you want. Google’s front end terminates TLS for thousands of hostnames and must pick a certificate before any encryption exists — so the hostname is the one thing every router, hotel Wi-Fi box, and national firewall on the path still learns about you. Encrypted ClientHello is the in-deployment fix: a decoy name on the outside, the real one encrypted under a key published in DNS.
The ServerHello returns the server’s own key share, and here is the trick: each side combines its private key with the other’s public share and arrives at the same 256-bit number — computed on both ends, transmitted on neither. HKDF stretches that shared secret into a schedule of keys, and everything after the ServerHello — including the certificate itself — is already encrypted. The wire goes dark mid-handshake.
Encrypted is not authenticated — this could all be a very secure conversation with an impostor. So the client walks the certificate chain: the leaf (CN=*.google.com, ~90-day lifetime) is signed by an intermediate, which is signed by a root that shipped inside your OS trust store — a list of roughly 150 organizations your OS vendor decided may vouch for anyone on earth. Signature, validity window, name match, Certificate Transparency logs: any failure and you get the red interstitial instead of a website.
Owning the certificate proves nothing — anyone can download it. So CertificateVerify signs a hash of every handshake byte said so far with the certificate’s private key, and Finished seals the transcript from both sides, proving both ends derived identical keys from the same conversation. The client’s Finished doesn’t even travel alone: the HTTP request is glued behind it in the same flight, under fresh application keys. Total: one round trip.
The ECDH keys were ephemeral — generated for this handshake, destroyed after it. Steal Google’s certificate key next year, subpoena every hard drive: today’s captured traffic stays noise. That’s forward secrecy. The bulk cipher, AES-128-GCM, is an — it authenticates as it encrypts — and AES-NI runs it at memory speed. Parting gift: a session ticket, so next visit the request can ride the ClientHello itself. 0-RTT is replayable, though, so servers only honor it for requests that are safe to repeat.
legacy_version is frozen at 0x0303 and a fake session ID is echoed back, because ossified middleboxes dropped anything that looked like a new protocol — the real version negotiation hides in an extension. When the client’s group guess misses, the server answers with a HelloRetryRequest and the handshake pays the extra round trip 1.3 was designed to avoid. And the HKDF schedule is bigger than “a key”: separate client/server secrets for handshake and application data, an exporter secret, and a resumption master secret — the last one becomes the session ticket that powers next visit’s 0-RTT.The tunnel is built — encrypted, authenticated, already warm. Now the actual errand: one compressed request, one detour every canonical answer forgets, and finally the first bytes of HTML. Everything until now was overhead. This is the chapter the other six were for.
ALPN said h2, so there is no GET / HTTP/1.1 text — the request is one binary HEADERS frame on stream 1, compressed with . Common headers live in a 61-entry static table baked into the spec, so :method: GET costs exactly one byte on the wire. The whole request — method, scheme, host, path — fits in about fifty bytes. No body to send, so the frame carries END_STREAM: the client has already said everything it came to say.
Riding along: capability headers (accept-encoding: gzip, br, zstd — the compressions you speak), identity headers (user-agent), provenance metadata (sec-fetch-mode: navigate — “a human typed this, it’s not some script”), and the cookies. The NID cookie is where “being logged in” physically lives, and at a few hundred bytes it outweighs the actual request many times over. You are mostly metadata.
google.com does not serve the page. The answer is 301 Moved Permanently → https://www.google.com/, and every “what happens when you press Enter” answer skips it. Here’s the part worth knowing: the certificate you verified in chapter 04 covers both names, so the browser coalesces — the “new” origin rides the connection you already have. No new DNS, no new TCP, no new TLS. The detour costs one round trip, not four chapters.
The retry opens stream 3 on the same connection — client-initiated streams take odd numbers, and 1 is spent. This is h2’s whole trick: a stream isn’t a thing, it’s just a number stamped on frames, so one TCP connection carries any number of interleaved conversations. When the page later wants CSS, fonts, and a logo, they’ll be streams 5, 7, 9 — in flight simultaneously, no queueing for a free connection like the HTTP/1.1 days.
The response headers land: content-encoding: br (Brotli), cache-control: private, max-age=0 — the very header that made chapter 01’s cache lookup miss — a set-cookie rotation, and alt-svc: h3=”:443”: an advertisement that this server speaks HTTP/3, so the next visit can skip TCP entirely. Time since your key hit the matrix in chapter 00: roughly 150–250 ms.
DATA frames at last — Brotli-compressed HTML squeezed through two throttles at once: h2’s flow-control window and TCP’s from chapter 03. That initial ~14 KB congestion window means the first server flight carries about one screen’s worth of page — which is exactly why critical CSS gets inlined in the <head> by people who know this number. And the renderer doesn’t wait for the last frame: chapter 07 starts parsing the moment the first one arrives.
ip_rcv → tcp_rcv_established, gets ACKed and queued to the socket, and the browser’s network process read()s it. The response body answers to both h2 flow-control windows and TCP’s — two independent brakes on the same stream, which is why “why is my download slow” has never once had a simple answer.Your packets did not go to “a server.” They went to an address that is a question, answered by whichever corner of a planetary machine sits closest to you. This chapter runs entirely inside the ~100 ms that chapter 05 spent waiting — the part of the story you never see.
Your packets went to 142.250.72.14 — which is not a place. means that address is announced via BGP from roughly a hundred points-of-presence at once, and internet routing itself delivers you to the topologically nearest one. Someone in Tokyo and someone in Toronto are “on google.com” at the same IP, talking to different continents. The address isn’t a destination; it’s a question the routing table answers differently for everyone.
First machine inside: a Maglev-style layer-4 balancer. It doesn’t terminate TLS, doesn’t parse HTTP — it hashes your connection’s 5-tuple and forwards packets at line rate. The hash is consistent: every packet of your flow lands on the same backend, and because the mapping is computed rather than remembered, a balancer can die mid-connection and its replacement sends your very next packet to the same place. State that was never stored can’t be lost.
The front-end proxy (Google calls its own the GFE) is the machine from chapter 04 — the other end of your encrypted tunnel physically lives here, at the edge, not in the data center. It decrypts, absorbs DDoS traffic, speaks h2/h3 outward — and from here inward your request stops being HTTP at all: it becomes an authenticated, encrypted RPC on a private backbone, stamped with a trace ID that will follow every hop of the request tree.
The web tier parses the request, turns the NID cookie from chapter 05 into an identity, picks your language and region — and assigns your experiment arms. You are in dozens of A/B tests on every request you have ever sent; this is the machine that decides which version of reality you get. Then it does the only thing a coordinator can do in a 150 ms budget: fan out, in parallel, to everything below.
Index shards, ranking models, spelling, ads, knowledge graph — each subsystem is itself thousands of leaf machines, and each leaf gets a deadline in the tens of milliseconds. There is no waiting: a straggler gets a hedged duplicate request sent to a second replica (first answer wins), and whoever misses the deadline is simply left out. The page you receive is the set of answers that showed up on time.
Results merge, the HTML template renders, Brotli squeezes it — and here’s the trick that saves your time-to-first-byte: the response starts streaming while later parts are still being computed. The <head>, the critical CSS, the top of the page leave the building before the bottom exists. Total server-side budget, door to door: ~50–150 ms — about what chapter 00 spent debouncing a spring.
One process was hired for this moment and sandboxed like it’s dangerous — because it is: it’s about to run a stranger’s code. Bytes are still arriving from chapter 05 as it starts. Watch HTML become a tree, the tree become geometry, geometry become instructions, and instructions — at the very last step — finally become light.
Chapter 05’s DATA frames land in a sandboxed renderer process that does not wait for the document to finish: the HTML tokenizer starts chewing on the very first chunk. And it isn’t even the first reader — the races ahead of the real parser doing nothing but spotting URLs, so the CSS and the logo were requested before the they belong to exists. The parser is thorough; the scanner is fast; the browser wants both.
Tokens become the DOM tree under error recovery so complete it’s in the standard — unclosed tags, misnested elements, and twenty years of broken HTML all produce a defined tree, which is why “view source” on the real web is horror and the web still works. One thing stops this machine dead: a classic <script> tag. V8 must compile and run it right there, because the script may document.write() into the parser’s mouth. defer and async exist to buy this moment back.
Stylesheets parse into the CSSOM, and here the browser chooses to be slow on purpose: CSS is render-blocking, because painting with half the rules means flashing the wrong page at you and repainting — worse than showing nothing. This is the last stop for chapter 05’s 14 KB trick: critical CSS inlined in the first flight unblocks this gate immediately. Then the cascade runs — specificity, inheritance, custom properties — and every one of thousands of nodes gets its final computed style.
Layout — — solves for the exact position and size of every box: flex, grid, floats, line breaking, writing modes, all constraint-solving over thousands of nodes. It’s expensive enough to have spawned its own genre of bug: write a style, then read offsetWidth, and JS forces the whole solve to run synchronously, right now, because the answer must be current. Do it in a loop and you’ve invented layout thrashing.
Paint produces no pixels either. It records display lists — draw commands per layer: fill this rectangle, stroke this border, draw this shaped glyph run at subpixel position x. Text shaping happens here (turning characters into positioned glyphs — hard enough in any script that isn’t ASCII), decorations get recorded, and the result is a compact to-do list for hardware that hasn’t been consulted yet.
The thread rasterizes tiles on the GPU and assembles layers into a frame — on its own thread, which is the single most user-visible architecture decision in the browser: scrolling and transform/opacity animations stay at 60 fps even while the main thread is wedged by JavaScript. The frame waits for vsync (16.7 ms at 60 Hz), the panel latches it, and photons leave the glass. Total, from the spring under your Enter key to light: ~300 milliseconds. You blink slower than that.
A warm-ish load from a decent connection, in one bar (~300 ms wall clock):
All of it — interrupts, recursion, handshakes, a planet-scale fan-out, and a GPU — in about the time of a blink (~350 ms).
EOF