THE INTERVIEW QUESTION, ANSWERED PROPERLY

What happens when you type

https://google.com

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

00

The keypress

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.

STEP 01 / 06
THE KEY IS LYING TO YOU
row × column matrix · swept ~1,000×/s
the switch, on an oscilloscope

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.

debounce ≈ 70% of this chapter’s total latency
STEP 02 / 06
A POSITION, NOT A CHARACTER
USB interrupt IN endpointhost polls every 1 ms
00
00
28
00
00
00
00
00
↑ modifierssix key slots — a 7th key ghosts ↑
0x28 = Enter — a key position, not a character

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.

six key slots per report — that’s why cheap boards ghost on the 7th key
STEP 03 / 06
THE RUDEST MICROSECOND
inside the sliver~2 µs · everything else is paused
save registers · read report · queue softirq · return
whatever the CPU was doing
…resumes, none the wiser
debounce (step 1)
~3,000 µs
IRQ handler
~2 µs
same latency story, drawn to scale

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.

handler ~2 µs vs debounce ~3,000 µs — a 1,500× gap
STEP 04 / 06
DVORAK IS A LOOKUP TABLE
scancode 0x28
keymap
lives in the OS
KEY_ENTER
$ cat /dev/input/event3
EV_KEY KEY_ENTER 1
swap QWERTY for Dvorak — the keyboard never changes a byte
on Windows: win32k queues a message instead

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.

switch layouts and not a single byte in the keyboard changes
STEP 05 / 06
TWO WINDOWS NEVER FIND OUT
terminal
never sees it
chat
never sees it
browser● focus
keydown arrives here
one keypress event · one recipient

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.

focus routing is also why keyloggers need special privileges
STEP 06 / 06
THE BROWSER WAS ASLEEP THE WHOLE TIME
epoll_wait()
0% CPU · zzz
the browser main thread
fd readable
keydown
keypress
⏎ commit omnibox
t ≈ 5 ms
packets sent: 0

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.

t ≈ 5 ms — and no packet exists yet
deep dive: interrupt → process, in syscalls
The browser is blocked in 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.
01

URL parsing & pre-flight

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.

STEP 01 / 06
SEARCH BAR OR ADDRESS BAR? YES.
>google.com
· has a dot· no spaces· known eTLD
→ NAVIGATE
⌕ SEARCH
the public-suffix list decides what counts as a website

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.

“google .com” — one space, and you’d have googled google.com instead
STEP 02 / 06
THE ALPHABET IMPOSTOR
what you seeаpple.com← U+0430, Cyrillic
what DNS gets
mixed script ⇒ displayed as raw punycode
ours: google.com — pure ASCII ✓

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.

аpple.com and apple.com differ by exactly one codepoint
STEP 03 / 06
A LIST BAKED INTO THE BINARY
no scheme typed, so…
http?//
packets used: 0
HSTS preload list · ships in the binary
adobe.com
dropbox.com
gmail.com
google.com
paypal.com
wikipedia.org

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 fix for a whole attack class ships as a list inside the browser
STEP 04 / 06
ONE TRUE SPELLING
whatever this is:
HTTPS://GoOgLe.CoM:443//
— scheme— host, lowercased(:443 now implied)— path, defaulted
this exact string = cache key + connection-pool key

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.

this exact string is the cache key and the connection-pool key
STEP 05 / 06
THREE CACHES, THREE MISSES
memory cache~1 µsMISS
disk cache~1 msMISS
service workernone registeredMISS
→ network
on a HIT: skip to ch 07
Cache-Control: private, max-age=0 — nothing to reuse

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.

a fresh cache hit would skip chapters 02–06 entirely
STEP 06 / 06
YOUR ENTER WAS THE LAST TO KNOW
you
google.com
DNS
TCP
TLS
⏎ pressed — already connected ✓
speculative pre-connect, started mid-keystroke

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.

the omnibox predicted this navigation before you committed to it
deep dive: why HSTS preload matters
The preload list ships inside the browser binary, so even the first ever visit is upgraded to HTTPS — the classic SSL-strip attack (intercept the initial http:// hop, keep the victim on plaintext) has nothing to strip. handling is the other quiet security check here: mixed-script lookalike domains get displayed as raw xn-- to fight homoglyph phishing.
02

DNS resolution

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.

STEP 01 / 06
FOUR CACHES STAND BETWEEN YOU AND THE WIRE
google.com A? — anyone remember this one?
browserdns cacheMISS
OS stubresolverMISS
hosts/etc/hostsMISS
routercacheMISS
→ to the wire, port 53
any HIT ends the chapter at ~0 ms

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.

most lookups die in these caches — the full cold walk is the rare path
STEP 02 / 06
A QUESTION IN 40 BYTES
header ·id 0x3f21RD=1
id + port randomized · anti-spoofing
QNAME — the name, as length-prefixed labels:
06google03com00
no dots on the wire — lengths instead
QTYPE=A → sent, ~40 bytes× AAAA twin, in parallel

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.

the dot in google.com never leaves your machine
STEP 03 / 06
THIRTEEN SERVERS THAT ARE ACTUALLY 1,900
the root zone ( . ) — 13 names, ~1,900 anycast servers
a
b
c
d
e
f
g
h
i
j
k
l
m
↑ “d.root-servers.net” — probably a rack in your city
→ google.com A?
← “no idea — but ask these:”
NS: .com servers ✓ · referral cached ~2 days

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.

genuine root hits are rare — the referral is cached for ~2 days
STEP 04 / 06
THE REGISTRY THAT ANSWERS FOR HALF THE WEB
.com registry · Verisign · trillions of queries/day
→ google.com A?
NS ns1.google.com
NS ns2.google.com
NS ns3.google.com
NS ns4.google.com
wait — resolving google.com requires… resolving google.com?
glue: ns1 = 216.239.32.10· loop avoided

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.

glue records exist because ns1.google.com is itself a name that needs resolving
STEP 05 / 06
THE ANSWER IS A LOAD-BALANCING DECISION
ns1.google.com answers — but first, it chooses:
142.250.72.14near your resolver
142.250.185.78eu-west
172.217.31.14asia
↑ picked for your resolver’s location ✓
google.com.300INA142.250.72.14
traffic engineering, before google.com has received a single byte from you

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.

two people in two cities get two different “google.com”s
STEP 06 / 06
CACHED ALL THE WAY BACK DOWN
A 142.250.72.14 · heading home ↓
resolver8.8.8.8TTL 300 s ✓
OS stubyour machineTTL 300 s ✓
browserper-tab cacheTTL 300 s ✓
300 s… then all of this again

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.

TTL 300 s — Google reserves the right to re-route you every five minutes
deep dive: the datagram's fine print + modern variants
The header flags carry 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.
03

TCP handshake

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.

STEP 01 / 06
A CONNECTION IS JUST FOUR NUMBERS
connect() — the kernel fills in the last blank:
192.168.1.7
your IP
:?????
ephemeral port
142.250.72.14
their IP
:443
https
picked from the ephemeral range 32768–60999
the 4-tuple — the connection’s entire identity, 12 bytes
state: CLOSED · SYN segment built, not yet sent

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 entire identity of a TCP connection fits in 12 bytes
STEP 02 / 06
TERMS NEGOTIATED ONCE, BINDING FOREVER
seq = x (random ISN)win 65535
URGACKPSHRSTSYNFIN
options — the contract, no renegotiation:
MSS 1460biggest segment that fits
SACK ✓resend only what was lost
WScale 7windows up to 8 MB
TSa running RTT clock
SYN → off to :443

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 .

to change MSS on a live connection, you hang up and call again
STEP 03 / 06
THE COUNTER-OFFER THAT REMEMBERS NOTHING
← SYN-ACK seq=y ack=x+1 — y is random, on purpose
server :443half-open state kept: 0 bytes
meanwhile:SYNSYNSYNSYNSYN← forged, no ACK ever comes
SYN cookie: the connection state lives inside y — math, not memory
your ACK returns, decodes cleanly → now it gets RAM

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.

a flooded server stores your connection inside a sequence number
STEP 04 / 06
THREE SEGMENTS, ONE SHARED FICTION
client :54321server :443
SYN seq=x
SYN-ACK seq=y ack=x+1
ACK ack=y+1 (+ ClientHello, same flight)
ESTABLISHEDESTABLISHED
two machines, two counters, one round trip

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.

ESTABLISHED means: we agree on two counters — nothing more
STEP 05 / 06
THE NETWORK NEVER TELLS YOU ITS SPEED
slow start — probe by doubling:
RTT 1
10 seg · 14.6 KB
RTT 2
20 seg
RTT 3
40 seg
RTT 4
80 seg
…until loss (CUBIC) or measured bandwidth (BBR) says stop
the first reply must fit in 14.6 KB to arrive in one round trip

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.

this is where the “keep your page under 14 KB” folklore comes from
STEP 06 / 06
CHROME ALREADY LEFT THE BUILDING
TCP + TLS — this story2 RTT to the request
SYN ⇄
TLS hello ⇄
request →
QUIC — Chrome ⇄ Google, UDP1 RTT · resumed: 0
transport + crypto + request, one flight ⇄
chapters 03 + 04: folded into one — you just read the fallback path

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.

QUIC folds chapters 03 and 04 into a single round trip
deep dive: when the handshake goes wrong
A lost SYN hurts more than any other loss: with no RTT estimate yet, the retransmission timer starts at a conservative 1 s and doubles — which is why a flaky network makes 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.
04

TLS 1.3 handshake

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.

STEP 01 / 06
THE GAMBLE THAT SAVES A ROUND TRIP
ClientHello — message #1, no keys yet
versions1.3 (disguised as 1.2)
ciphersAES-GCM · ChaCha20…
SNIgoogle.com
ALPNh2, http/1.1
key_sharex25519 pub · 32 B
↑ the gamble: key share sent before the server agreed to anything
an eavesdropper reads every field of this

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.

TLS 1.2 paid an extra round trip just deciding how to agree
STEP 02 / 06
YOUR LAST CLEARTEXT SENTENCE
you
👁 every box on the path
google
ClientHello · SNI = google.com ← readable, by design
with ECH · outer = decoy.example
inner = ████████████ · key came from DNS
the front end must pick 1 of ~thousands of certs before decryption exists

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 hostname is the last thing the network still reads about you
STEP 03 / 06
TWO STRANGERS, ONE SECRET, ZERO SECRETS SENT
client
private: a (never leaves)
a·B = 9f3a…c41d
public A ⟶⟵ public Bthe only things sent
server
private: b (never leaves)
b·A = 9f3a…c41d
same number, computed on both ends — transmitted on neither
HKDF ↓handshake keys ✓ — wire goes dark

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.

the shared secret is computed twice and transmitted zero times
STEP 04 / 06
THE PART WHERE YOU TRUST ~150 STRANGERS
CN=*.google.comleaf · valid ~90 days
GTS CA 1C3intermediate
GTS Root R1in your OS since before you bought it
↑ each link: “signed by the one above”
signature ✓dates ✓name ✓CT log ✓
any ✗ here → the red full-page interstitial

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.

your OS ships a list of ~150 organizations allowed to vouch for the entire internet
STEP 05 / 06
PROOF, THEN PAYLOAD, SAME BREATH
ClientHelloServerHelloCertificatetranscript hash
CertificateVerify: sign(transcript, cert private key) ✓
← Finished·Finished →same keys, both proven
GET / — glued to the same flightapplication keys live

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 GET is sent before the handshake even feels finished
STEP 06 / 06
RECORD TODAY, DECRYPT NEVER
captured today:
e2 91 4f b8 03 d7 6a c5 …
🔑 cert key, stolen next year→ decrypts: nothing ✗
the real keys were ephemeral — destroyed after the handshake
next visit:session ticket → GET rides the ClientHello0-RTT

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.

0-RTT data can be replayed — it’s only trusted for idempotent requests
deep dive: the 1.2 costume, HelloRetryRequest & the key schedule
On the wire, TLS 1.3 wears a TLS 1.2 costume: 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.
05

The HTTP exchange

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.

STEP 01 / 06
THE ONE-BYTE GET
HEADERS · stream 1 · END_STREAM — the entire request
:method: GET82static table #2 — 1 B
:scheme: https87static table #7 — 1 B
:path: /84static table #4 — 1 B
:authority: google.com41 88 …literal — 12 B
h2 total: ~50 BHTTP/1.1 text: ~700 B
both sides ship the same 61-entry table — that’s the whole trick

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.

“:method: GET” is one byte — index 2 in a table both sides memorized
STEP 02 / 06
YOUR COOKIES WEIGH MORE THAN YOUR REQUEST
what rides along with 7 bytes of intent:
user-agent
~120 B
accept / accept-encoding
~130 B
cookie: NID=…
~400 B
sec-fetch-*
~50 B
the cookie alone outweighs the request ~50×
cookie: NID=… — this is where “logged in” physically lives

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.

the request is ~7 bytes of intent wrapped in ~700 bytes of context
STEP 03 / 06
THE REDIRECT EVERY INTERVIEW ANSWER FORGETS
← :status 301 · location: https://www.google.com/
new DNS?new TCP?new TLS?
cert from ch 04 covers www too → same connection, coalesced
handshakes re-paid: 0 · cost: one round trip

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 redirect re-runs chapters 02–04 zero times — coalescing eats it
STEP 04 / 06
TAKE TWO, SAME PIPE
stream 1 — spentodd numbers, client picks the next:3
one TCP connection · frames interleaved:
HEADERSstr 3
DATAstr 3
HEADERSstr 5
DATAstr 3
HEADERSstr 7
soon: CSS on 5, font on 7, logo on 9 — all in flight at once
HTTP/1.1 would open six connections for this

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.

a stream is not a connection — it’s a number stamped on frames
STEP 05 / 06
200, AND A NOTE ABOUT NEXT TIME
:status: 200finally
content-encoding: brBrotli — ~5× smaller
cache-control: private, max-age=0the ch 01 cache miss, explained
set-cookie: NID=… (rotated)your identity, refreshed
alt-svc: h3=":443"“next time, skip TCP entirely”
time since the keypress in ch 00: ~150–250 ms
headers only — still not one byte of HTML

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.

alt-svc is the server saying “next time, chapters 03–04 are optional”
STEP 06 / 06
FOURTEEN KILOBYTES OF DESTINY
two brakes on one stream:h2 windowTCP cwnd (ch 03)
← DATA
↖ flight #1: ~14 KB — why critical CSS gets inlined in <head>
meanwhile: the renderer is already parsing — ch 07 has begun

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.

chapter 07 begins before this chapter finishes — parsing starts on frame one
deep dive: HPACK's dynamic table + the kernel receive path
has a second table: a dynamic one, built per-connection from headers already sent — so your 400-byte cookie costs its full weight once, then one byte on every later request. Kernel-side on receive: each arriving segment raises a (coalesced) NIC interrupt, lands in a ring buffer via DMA, climbs 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.
06

Meanwhile, on the server side

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.

STEP 01 / 06
THE IP ADDRESS THAT ISN’T ANYWHERE
142.250.72.14 — announced from all of these at once:
tokyo96 ms
frankfurt12 ms
virginia89 ms
são paulo184 ms
↑ BGP hands you to the topologically nearest — no one chose this, routing did
the address is a question — everyone gets a different answer

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.

the same IP answers in Tokyo and Toronto — from different machines
STEP 02 / 06
A BALANCER THAT NEVER READ YOUR DATA
pkts →maglev-1maglev-2— either one:
hash(src ip · src port · dst ip · dst port · proto) = b3
b1
b2
b3
b4
maglev-1 died mid-flow — the math still says b3 ✓
state that was never stored can’t be lost

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 balancer can die mid-connection and your flow never notices
STEP 03 / 06
WHERE YOUR TLS ACTUALLY ENDS
you ═ TLS ═►
GFE
ch 04’s other end — at the edge, not the data center
decrypt (your keys live here)DDoS filterh2/h3 demux
onward, as something else:RPC · trace 7f3d9a01 · authenticated · encrypted
that trace ID will follow every hop your request causes

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.

past this box, “HTTP” no longer exists — it’s RPCs all the way down
STEP 04 / 06
THE PART THAT KNOWS WHO YOU ARE
parse queryq = "" (homepage)
cookie → identityNID ⇒ you, signed in
localelang=en · region=…
experimentsarms: 17B · 42A · 9C …
indexrankingadsgraph
all RPCs leave in parallel — a coordinator on a deadline can’t afford turns

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.

every request you send is enrolled in dozens of experiments
STEP 05 / 06
A FEW THOUSAND MACHINES RACE A DEADLINE
each leaf races the deadline:deadline ~30 ms ↓
index shard 0..999
✓ in time
ranking / ML
✓ in time
spelling
✓ in time
ads
…slow
knowledge graph
…slow
the page = whatever answered in time · hedge: ask a second replica, keep the faster

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.

slow answers aren’t waited for — the page ships without them
STEP 06 / 06
STREAMING BEFORE IT’S FINISHED
merge resultsrender templatebrotli→ first bytes out
← streaming: <head> · critical CSS · top of page
rest: still computing…
server total: ~50–150 ms — everything in this chapter
chapter 05’s DATA frames — this is where they came from

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.

the first byte left the building before the page existed
deep dive: tail latency — the math that shapes the stack
The fan-out has unforgiving math: if each leaf is slow just 1% of the time, a request touching 100 leaves hits a slow one 63% of the time — at 1,000 leaves, p999 is the user experience. Hence the toolkit: tight per-leaf deadlines, hedged duplicates to a second replica after a few milliseconds of silence, and shipping best-effort results without stragglers. Every internal hop is mutually authenticated and encrypted (Google’s ALTS), carrying the one trace ID that lets an engineer replay your request’s whole tree. And the honest footnote: for a homepage (vs. a results page), much of this is a per-locale pre-rendered shell from cache with personalized bits injected per-cookie — the full army mobilizes when you actually search.
07

The render pipeline

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.

STEP 01 / 06
PARSING STARTS BEFORE THE PAGE ARRIVES
DATA frames (ch 05) →tokenizer— starts on chunk #1
<html><head><link css><body><img logo>
preload scanner, racing ahead:
fetch styles.css!fetch logo.png!
two readers: the parser is thorough, the scanner only hunts URLs

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.

the preload scanner ordered the CSS before the DOM existed
STEP 02 / 06
THE MOST FORGIVING PARSER EVER SPECIFIED
html
├─ head
│ └─ link · title
└─ body
└─ script ⟵
└─ …rest of the page
parser: running
V8: compile + run — it might document.write()
defer / async: the escape hatches
error recovery is specified — broken HTML builds the same tree everywhere

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.

HTML error recovery is standardized — broken pages parse identically everywhere
STEP 03 / 06
NOTHING PAINTS WITH HALF THE RULES
render gate: OPENevery rule accounted for
critical CSS — inlined in ch 05’s first 14 KB → parsed immediately
the cascade, per node:
specificity ⊕ inheritance ⊕ custom props → computed style
× every one of ~3,000 nodes
painting with half the rules = flashing the wrong page — worse than nothing

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.

CSS blocks rendering by design — wrong styles are worse than no pixels
STEP 04 / 06
GEOMETRY IS THE EXPENSIVE PART
every box: exact x, y, width, height — solved
el.style.width = '50%'; el.offsetWidth // answer must be current…
⚠ forced synchronous layout — in a loop, that’s “layout thrashing”

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.

offsetWidth is a question so expensive it has its own bug genre
STEP 05 / 06
A PAINTING MADE OF INSTRUCTIONS
display list · layer 0draw commands, not drawing
00 fillRect(0, 0, 1280, 720, #fff)
01 drawGlyphRun("Google", x=562.5, shaped)
02 drawRRect(searchBox, r=24, stroke)
03 drawImage(logo, 490, 200)
pixels produced by “paint”: still exactly zero
text shaping happens here — characters became positioned glyphs

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.

paint outputs zero pixels — it writes a to-do list for the GPU
STEP 06 / 06
PHOTONS, FINALLY
GPU tilesvsync ✓ 16.7 ms
main thread: wedged by JScompositor thread: still 60 fps
⏎ → photons: ~300 ms · you blink slower than that

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.

keypress to photons: ~300 ms — a blink takes longer
deep dive: the sandbox around all of this
The sandbox is the quiet hero: each site gets its own renderer process (Site Isolation) behind a Spectre-hardened boundary, so google.com’s process cannot read your bank tab’s memory — not even speculatively. The renderer can’t open sockets or files at all; every fetch in this chapter was brokered over IPC to the network process you met in chapter 05. The compositor’s independence has a sharp edge, though: it can only move what it already has. Transform and opacity changes ride the GPU for free, but anything that dirties layout re-enters the main-thread pipeline at step four and waits its turn behind whatever JavaScript is doing.
08

Σ  the whole budget

A warm-ish load from a decent connection, in one bar (~300 ms wall clock):

DNS
TCP
TLS
SERVER (TTFB)
DOWNLOAD
RENDER
0 ms~150 ms~300 ms · first paint
  • Repeat visit collapses the left half: DNS cached, connection pooled or 0-RTT resumed — straight to the request.
  • The single biggest lever is distance: every sequential round trip pays the same RTT tax — edge PoPs, TLS 1.3, and QUIC exist to delete round trips.

All of it — interrupts, recursion, handshakes, a planet-scale fan-out, and a GPU — in about the time of a blink (~350 ms).
EOF