# What Actually Happens When You Open a Web Page

> Type a URL, press Enter: DNS, TCP, TLS, HTTP/2 and HTTP/3, a CDN edge and the browser's rendering path, traced with real dig and curl output.

- Author: Tai Pham Phu
- Published: 14 Nov 2026
- Tags: Web, Networking, Software Design, Learning, Performance
- URL: https://phamphutai.com/2026/11/14/what-happens-when-you-open-a-web-page/

Type `phamphutai.com` into the address bar and press Enter. A page appears. It feels like one action.

It isn't. Before the first word shows up, your browser has asked a chain of servers where the site lives, opened a connection to a machine in another country, agreed on encryption keys with it, checked a certificate, sent a request, received a compressed reply from a cache, and turned that reply into pixels. A dozen separate systems, owned by different organizations, in a fraction of a second.

This post walks through every step, in order, for developers and curious builders. Every term is explained once, and the example is a real site: this one. At the end you'll run the same commands I ran.

---

## The whole journey on one page

<figure class="diagram">
<svg viewBox="0 0 680 452" role="img" aria-label="Seven steps from pressing Enter to a rendered page: parse the URL and check HSTS, look up the address with DNS, open a TCP connection, run the TLS handshake, send the HTTP request, get an answer from the edge cache or Worker, and render the page." xmlns="http://www.w3.org/2000/svg">
<defs><marker id="web-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker></defs>
<text x="130" y="22" font-size="11" text-anchor="middle" class="muted">STEP</text>
<text x="300" y="22" font-size="11" class="muted">WHAT HAPPENS</text>
<rect x="30" y="34" width="200" height="40" rx="6" fill="currentColor"/>
<text x="130" y="59" font-size="13" text-anchor="middle" class="on-fg">1 URL + HSTS</text>
<text x="300" y="59" font-size="13">browser decides: https, this host</text>
<line x1="130" y1="74" x2="130" y2="92" stroke="currentColor" marker-end="url(#web-arrow)"/>
<rect x="30" y="94" width="200" height="40" rx="6" fill="none" stroke="currentColor"/>
<text x="130" y="119" font-size="13" text-anchor="middle">2 DNS lookup</text>
<text x="300" y="119" font-size="13">name to IP address (often cached)</text>
<line x1="130" y1="134" x2="130" y2="152" stroke="currentColor" marker-end="url(#web-arrow)"/>
<rect x="30" y="154" width="200" height="40" rx="6" fill="none" stroke="currentColor"/>
<text x="130" y="179" font-size="13" text-anchor="middle">3 TCP handshake</text>
<text x="300" y="179" font-size="13">SYN, SYN-ACK, ACK: one round trip</text>
<line x1="130" y1="194" x2="130" y2="212" stroke="currentColor" marker-end="url(#web-arrow)"/>
<rect x="30" y="214" width="200" height="40" rx="6" fill="none" stroke="currentColor"/>
<text x="130" y="239" font-size="13" text-anchor="middle">4 TLS 1.3</text>
<text x="300" y="239" font-size="13">keys + certificate: one round trip</text>
<line x1="130" y1="254" x2="130" y2="272" stroke="currentColor" marker-end="url(#web-arrow)"/>
<rect x="30" y="274" width="200" height="40" rx="6" fill="none" stroke="currentColor"/>
<text x="130" y="299" font-size="13" text-anchor="middle">5 HTTP request</text>
<text x="300" y="299" font-size="13">GET / over HTTP/2 or HTTP/3</text>
<line x1="130" y1="314" x2="130" y2="332" stroke="currentColor" marker-end="url(#web-arrow)"/>
<rect x="30" y="334" width="200" height="40" rx="6" fill="none" stroke="currentColor"/>
<text x="130" y="359" font-size="13" text-anchor="middle">6 Edge</text>
<text x="300" y="359" font-size="13">nearest data centre: cache, Worker</text>
<line x1="130" y1="374" x2="130" y2="392" stroke="currentColor" marker-end="url(#web-arrow)"/>
<rect x="30" y="394" width="200" height="40" rx="6" fill="currentColor"/>
<text x="130" y="419" font-size="13" text-anchor="middle" class="on-fg">7 Render</text>
<text x="300" y="419" font-size="13">HTML, CSS, JS, fonts become pixels</text>
</svg>
<figcaption>Seven steps between pressing Enter and seeing a page; steps 2 to 4 often run before a single byte of the page is sent.</figcaption>
</figure>

---

## Step 1: Reading the URL, and HSTS

First the browser works out what you typed. Is it a search query or an address? `phamphutai.com` has a dot and no spaces, so it's treated as an address. A full **URL** (Uniform Resource Locator) has parts: a **scheme** (`https`), a **host** (`phamphutai.com`), an optional **port**, a **path** (`/`), and sometimes a query string after `?`.

You didn't type a scheme, so which one? Modern browsers try `https` first. But there's an older safety net called **HSTS** (HTTP Strict Transport Security, RFC 6797). A site sends a header like this:

```
strict-transport-security: max-age=31536000; includeSubDomains
```

That tells the browser: **for the next year (31,536,000 seconds), never talk to this host or its subdomains over plain HTTP.** After the first visit, the browser upgrades every `http://` link to `https://` by itself, before any network traffic happens. The very first visit is still exposed, which is why Chrome maintains an **HSTS preload list** that's built into the browser. Firefox, Safari and Edge use lists based on it. To join, a site needs `max-age` of at least a year, `includeSubDomains` and a `preload` directive. My site sends the first two but not `preload`, so it relies on the normal header plus a redirect from `http` to `https`.

---

## Step 2: DNS, the internet's phone book

Computers on the internet find each other by **IP address**, a number like `104.21.6.41` (IPv4) or `2606:4700:3033::ac43:9ad0` (IPv6). Humans use names. The **Domain Name System** (DNS) turns one into the other.

Four kinds of server are involved:

- **Stub resolver.** A small piece of your operating system. It doesn't know answers; it asks someone who does. It also keeps a short-lived cache.
- **Recursive resolver.** Usually run by your internet provider, or a public one like Cloudflare's `1.1.1.1` or Google's `8.8.8.8`. It does the legwork of finding the answer and caches it for everyone who asks next.
- **Root servers.** The top of the tree. They don't know where `phamphutai.com` is, but they know who runs `.com`. There are 13 root server identities (`a` to `m`) run by 12 independent operators, served from more than 2,000 physical instances around the world.
- **TLD and authoritative servers.** The **TLD** (top-level domain) servers for `.com` know which servers are responsible for `phamphutai.com`. Those **authoritative** servers hold the actual answer. For this site, that's Cloudflare's nameservers.

<figure class="diagram">
<svg viewBox="0 0 680 330" role="img" aria-label="A DNS lookup chain. The browser asks the stub resolver in the operating system, which asks a recursive resolver. The resolver asks a root server, which refers it to the .com TLD servers, which refer it to Cloudflare's authoritative nameservers, which return the A record. The answer flows back and is cached at each level." xmlns="http://www.w3.org/2000/svg">
<defs><marker id="web-arrow2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="currentColor"/></marker></defs>
<rect x="20" y="40" width="160" height="50" rx="6" fill="currentColor"/>
<text x="100" y="62" font-size="13" text-anchor="middle" class="on-fg">Browser + OS</text>
<text x="100" y="79" font-size="11" text-anchor="middle" class="on-fg">stub resolver</text>
<rect x="20" y="200" width="160" height="50" rx="6" fill="none" stroke="currentColor"/>
<text x="100" y="222" font-size="13" text-anchor="middle">Recursive</text>
<text x="100" y="239" font-size="11" text-anchor="middle" class="muted">ISP or 1.1.1.1</text>
<line x1="90" y1="92" x2="90" y2="196" stroke="currentColor" marker-end="url(#web-arrow2)"/>
<line x1="110" y1="198" x2="110" y2="94" stroke="currentColor" marker-end="url(#web-arrow2)"/>
<text x="60" y="150" font-size="11" text-anchor="end" class="muted">ask</text>
<text x="118" y="150" font-size="11" class="muted">answer</text>
<rect x="420" y="30" width="240" height="50" rx="6" fill="none" stroke="currentColor"/>
<text x="540" y="52" font-size="13" text-anchor="middle">Root (.)</text>
<text x="540" y="69" font-size="11" text-anchor="middle" class="muted">"ask the .com servers"</text>
<rect x="420" y="140" width="240" height="50" rx="6" fill="none" stroke="currentColor"/>
<text x="540" y="162" font-size="13" text-anchor="middle">.com TLD</text>
<text x="540" y="179" font-size="11" text-anchor="middle" class="muted">"ask ns.cloudflare.com"</text>
<rect x="420" y="250" width="240" height="50" rx="6" fill="none" stroke="currentColor"/>
<text x="540" y="272" font-size="13" text-anchor="middle">Authoritative</text>
<text x="540" y="289" font-size="11" text-anchor="middle" class="muted">"A 104.21.6.41, TTL 300"</text>
<line x1="182" y1="212" x2="416" y2="60" stroke="currentColor" marker-end="url(#web-arrow2)"/>
<line x1="182" y1="225" x2="416" y2="168" stroke="currentColor" marker-end="url(#web-arrow2)"/>
<line x1="182" y1="238" x2="416" y2="272" stroke="currentColor" marker-end="url(#web-arrow2)"/>
<text x="290" y="118" font-size="12" class="muted">1</text>
<text x="300" y="186" font-size="12" class="muted">2</text>
<text x="300" y="248" font-size="12" class="muted">3</text>
</svg>
<figcaption>The recursive resolver walks down the tree, root to TLD to authoritative, then caches the answer so the next person skips the walk.</figcaption>
</figure>

### Caching and TTL

Every answer comes with a **TTL** (time to live), in seconds, that says how long it may be cached. In the real lookup further down, the `.com` servers say Cloudflare is responsible for `phamphutai.com` with a TTL of 172,800 seconds (two days), while the site's own address has a TTL of 300 seconds (five minutes). Delegations change rarely; addresses can change more often.

**Most lookups never leave your machine or your resolver.** The full walk only happens when nothing along the way has a fresh copy.

### Record types

DNS stores more than addresses. The three you meet first:

| Record | Holds | Example |
|---|---|---|
| **A** | An IPv4 address | `phamphutai.com. 300 IN A 104.21.6.41` |
| **AAAA** | An IPv6 address | `2606:4700:3033::ac43:9ad0` |
| **CNAME** | "This name is an alias for that name" | `blog.example.com` points to `example.netlify.app` |

A CNAME makes the resolver start again with the new name, which adds a lookup. Note that `phamphutai.com` answers with two A records and two AAAA records. The browser can try any of them, and modern clients often race IPv6 and IPv4 and use whichever connects first.

### DNS over HTTPS

Classic DNS travels unencrypted, so anyone on the network path can read or tamper with your lookups. **DNS over HTTPS** (DoH, RFC 8484) sends the same queries inside ordinary encrypted HTTPS requests to a resolver. Browsers like Firefox and Chrome can use it. It hides your lookups from the café Wi-Fi, though the resolver you send them to still sees them.

---

## Step 3: Opening a connection

The browser now has an IP address. Next it needs a reliable channel to that machine.

### TCP: the three-way handshake

**TCP** (Transmission Control Protocol, RFC 9293) turns the internet's unreliable packets into an ordered, reliable stream of bytes. Before any data flows, the two sides agree to talk:

1. **SYN.** Client: "I'd like to connect, here's my starting sequence number."
2. **SYN-ACK.** Server: "OK, here's mine, and I got yours."
3. **ACK.** Client: "Got yours." The client can send data right after this.

That costs one **round trip** (RTT): the time for a message to go there and back. From Đà Nẵng to a data centre in Singapore, that's around 50 milliseconds in my measurements below. You pay it before anything useful happens.

### TLS 1.3: keys and a certificate

Next comes **TLS** (Transport Layer Security), the "S" in HTTPS. It does two jobs: **encrypt** the traffic so nobody in between can read or change it, and **prove** the server really is `phamphutai.com`.

TLS 1.3 (RFC 8446) does this in **one round trip**:

- The client sends a **ClientHello**: supported ciphers, a key share for a key exchange, and two important extensions. **SNI** (Server Name Indication) names the site it wants, because one IP address at a CDN serves thousands of sites. **ALPN** (Application-Layer Protocol Negotiation) lists the HTTP versions it speaks, like `h2` and `http/1.1`.
- The server answers with a **ServerHello** and its own key share. Both sides can now compute the same secret. RFC 8446 puts it plainly: "All handshake messages after the ServerHello are now encrypted." So the server's **certificate**, a signature proving it holds the matching private key, and a **Finished** message all travel encrypted.
- The client checks the certificate: is it for this name, is it in date, and was it signed by a **certificate authority** (CA) the device already trusts? Then it sends its own Finished, plus the request.

TLS 1.2 needed two round trips for the same thing. TLS 1.3 also offers **0-RTT** resumption, where a returning client sends data in its very first message, but the RFC warns that 0-RTT data can be replayed by an attacker, so it's only safe for requests that don't change anything.

The certificate on this site, from the curl output below: issued to `CN=phamphutai.com` by Google Trust Services, valid from 7 November 2026 to 5 February 2027. Short-lived certificates like this are renewed automatically by Cloudflare.

### HTTP/2 and HTTP/3

Inside the encrypted channel, the browser speaks **HTTP**. Which version depends on what ALPN agreed:

| Version | Runs over | Key idea |
|---|---|---|
| **HTTP/1.1** | TCP | One request at a time per connection, so browsers open several connections |
| **HTTP/2** (RFC 9113) | TCP + TLS | Many requests **multiplexed** as streams on one connection; compressed headers |
| **HTTP/3** (RFC 9114) | **QUIC** over UDP | Same streams, but a lost packet only stalls its own stream |

HTTP/2 fixed most of HTTP/1.1's waste, but it inherited one problem from TCP. TCP delivers bytes strictly in order, so one lost packet holds up every stream on the connection until it's resent. This is called **head-of-line blocking**.

**QUIC** (RFC 9000) is a new transport built on UDP. It moves reliability and ordering into each stream, so a lost packet only delays the stream it belonged to. And it merges the transport and TLS 1.3 handshakes into one, so a new connection is ready after a single round trip instead of two. HTTP/3 is HTTP running on top of QUIC.

How does a browser know a site speaks HTTP/3? The site tells it. This site's response includes `alt-svc: h3=":443"; ma=86400`: "HTTP/3 is available on port 443; remember this for a day." The first visit typically uses HTTP/2, and later connections can switch to HTTP/3.

---

## Step 4: The edge, where most of the work is done

That IP address, `104.21.6.41`, isn't a server in my house or a single machine anywhere. It belongs to **Cloudflare**, a **CDN** (content delivery network): a company with data centres in more than 300 cities that sits between visitors and websites.

### Anycast: one address, many places

Cloudflare uses **anycast**. The same IP address is announced from every data centre at once, using **BGP** (Border Gateway Protocol, the system networks use to tell each other which addresses they can reach). Internet routers send your packets along the shortest route they know, so you end up at a nearby data centre without anyone choosing it for you. The `cf-ray` header in a Cloudflare response ends with an airport code for the data centre that served it. My requests from Đà Nẵng came back stamped `SIN`: Singapore.

### Caching at the edge

A CDN keeps copies of files close to visitors. If the Singapore data centre already has the file, it answers immediately: a **cache hit**, shown as `cf-cache-status: HIT`. If not, it's a **miss**, and it fetches the file from the origin first.

### This site's setup

phamphutai.com is a static site built with **Astro**: at build time, every post becomes plain HTML, CSS and a little JavaScript in a `dist` folder. No application server, no database.

That folder is deployed as a **Cloudflare Worker with static assets**. A **Worker** is a small JavaScript function that runs in every Cloudflare data centre, right next to the cache. The configuration (`wrangler.jsonc`) does three things that matter here:

- `"assets": { "directory": "./dist" }` uploads the built site to Cloudflare's asset storage.
- `"run_worker_first": true` makes the Worker run before every request. By default, Cloudflare serves a matching file without running the Worker at all.
- Two **custom domains**, `phamphutai.com` and `www.phamphutai.com`, route traffic to it.

The Worker itself is about 30 lines. In order, it:

1. Redirects `http://` and `www.` requests to `https://phamphutai.com` with a **301** (permanent) redirect, so there's one canonical address for search engines.
2. Adds a trailing slash to paths like `/writing`, again with a 301.
3. For the Markdown copies of posts (`.md` files), adds a `Link: rel="canonical"` header pointing at the HTML page.
4. Otherwise hands the request to the static assets with `env.ASSETS.fetch(request)`.

**So for most requests, the "server" is a cache and a tiny function, a few hundred kilometres from the reader.** That's why a static site on a CDN is so fast and so cheap to run.

---

## Step 5: The HTTP request and response

The browser sends something like this (HTTP/2 encodes it in binary, but the meaning is the same):

```
GET / HTTP/2
Host: phamphutai.com
Accept: text/html
Accept-Encoding: gzip, br
```

The first word is the **method**. `GET` means "give me this," `HEAD` asks for headers only, `POST` sends data.

The response starts with a **status code**:

| Code | Meaning | You'll see it when |
|---|---|---|
| **200** | OK | The page is returned |
| **301** | Moved permanently | `www.` or `http://` gets redirected |
| **304** | Not modified | Your cached copy is still good |
| **404** | Not found | A typo in the URL |
| **5xx** | Server error | Something behind the edge broke |

Then come **headers**, the metadata. A few that matter for speed:

- **`content-type`** says what the body is: `text/html`, `text/css`, `image/png`. The browser trusts this, not the file extension.
- **`cache-control`** says who may cache the response and for how long. This site uses two very different rules. HTML pages get `public, max-age=0, must-revalidate`: the browser may keep a copy, but must check it's still current before using it, so a new post shows up immediately. CSS and JavaScript files under `/_astro/` get `public, max-age=31536000, immutable`: keep it for a year and never ask again. That's safe because Astro puts a hash of the content in the file name, like `Base.CL4LZlOo.css`. Change the file, and the name changes.
- **`etag`** is a fingerprint of the file. Next time, the browser sends it back in an `If-None-Match` header. If nothing changed, the server replies **304** with no body, and the browser uses its copy.
- **`content-encoding`** says how the body was compressed. Text compresses very well. This site's home page is 26,774 bytes raw and 5,173 bytes with **Brotli** (`br`), a compression format designed for the web. That's about **80% smaller**.

---

## Step 6: The browser turns bytes into pixels

Now the browser has HTML. The sequence of steps from there to a visible page is called the **critical rendering path**:

1. **Parse HTML into the DOM.** The **DOM** (Document Object Model) is a tree of every element on the page. The parser works as bytes arrive; it doesn't wait for the whole file.
2. **Parse CSS into the CSSOM.** The CSS Object Model holds every style rule. CSS is **render-blocking**: the browser won't paint until it has the stylesheets in the `<head>`, otherwise you'd see a flash of unstyled page.
3. **Build the render tree.** Combine the DOM and CSSOM, keeping only what's visible. An element with `display: none` is dropped.
4. **Layout.** Work out the exact size and position of every box for this screen width.
5. **Paint.** Fill in the pixels: text, colours, borders, images.
6. **Composite.** Combine painted layers in the right order and show them on screen.

**JavaScript** can interrupt all this. A plain `<script>` tag stops the HTML parser until the script downloads and runs, because the script might change the page. Adding `defer` or `async`, or using `type="module"`, lets parsing continue. **Fonts** can hide text until they load, unless the CSS uses `font-display: swap`. **Images** without a declared `width` and `height` make the page jump when they arrive.

### Core Web Vitals

Google measures how this feels to real users with three **Core Web Vitals**, judged at the 75th percentile of page loads:

| Metric | Measures | Good | Poor |
|---|---|---|---|
| **LCP** (Largest Contentful Paint) | When the biggest image or text block appears | ≤ 2.5 s | > 4.0 s |
| **INP** (Interaction to Next Paint) | How quickly the page responds to clicks and taps | ≤ 200 ms | > 500 ms |
| **CLS** (Cumulative Layout Shift) | How much content jumps around | ≤ 0.1 | > 0.25 |

INP replaced First Input Delay as a Core Web Vital in March 2024. Everything between "good" and "poor" counts as "needs improvement." Each step in this post feeds LCP: a slow DNS lookup, an extra round trip or an uncompressed file all push the largest element later.

---

## Where each step can break

Every layer has failed in public. One real example each:

| Layer | Incident | What happened |
|---|---|---|
| **DNS + BGP** | Facebook, 4 Oct 2021 | A maintenance command took down Meta's backbone. Its DNS servers then withdrew their BGP routes, so the internet couldn't even find facebook.com. Cloudflare saw it unreachable for roughly six hours, from about 15:40 to 21:28 UTC. |
| **CDN / edge** | Cloudflare, 18 Nov 2025 | A database permissions change doubled the size of a Bot Management configuration file. Cloudflare's proxy had a hard limit below the new size and failed, returning 5xx errors from about 11:20 UTC, fully resolved at 17:06 UTC. Cloudflare says it was not an attack. |
| **TLS** | Let's Encrypt root expiry, 30 Sep 2021 | The old DST Root CA X3 certificate expired. Older devices that didn't trust Let's Encrypt's newer ISRG Root X1 started showing certificate errors on sites that were otherwise fine. |

**My takeaway: the steps you never think about are the ones that take everything down at once.** Your code can be perfect and the page still won't load.

---

## Try it yourself

Everything above can be seen from a terminal. These commands are read-only and safe to run against any public site. The output below is real, trimmed, from one machine in Đà Nẵng on 14 November 2026. Your times and IP addresses will differ.

### 1. DNS: `dig`

```
$ dig phamphutai.com

;; ANSWER SECTION:
phamphutai.com.		300	IN	A	172.67.154.208
phamphutai.com.		300	IN	A	104.21.6.41

;; Query time: 545 msec
```

Two A records, each with a TTL of 300 seconds. That first query took 545 ms because the resolver had to do work; ask again and it comes back from cache. To watch the whole walk from the root down:

```
$ dig +trace phamphutai.com

.        169     IN  NS  a.root-servers.net.
...
com.     172800  IN  NS  a.gtld-servers.net.
...
phamphutai.com.  172800  IN  NS  daisy.ns.cloudflare.com.
phamphutai.com.  172800  IN  NS  cameron.ns.cloudflare.com.

phamphutai.com.  300     IN  A   172.67.154.208
phamphutai.com.  300     IN  A   104.21.6.41
;; Received 75 bytes from ...(cameron.ns.cloudflare.com) in 53 ms
```

Root, then `.com`, then Cloudflare's authoritative servers, exactly as in the diagram.

### 2. Headers: `curl -sI`

```
$ curl -sI https://phamphutai.com/

HTTP/2 200
content-type: text/html
cf-cache-status: HIT
cache-control: public, max-age=0, must-revalidate
strict-transport-security: max-age=31536000; includeSubDomains
server: cloudflare
cf-ray: a5a7539c1bdece6e-SIN
alt-svc: h3=":443"; ma=86400
```

HTTP/2, a cache hit at the Singapore edge, the HSTS policy, and an offer of HTTP/3. Try `curl -sI http://phamphutai.com/` to see the `301` redirect to HTTPS.

### 3. The handshake: `curl -sv`

```
$ curl -sv -o /dev/null https://phamphutai.com/ 2>&1 | head -40

* Connected to phamphutai.com (2606:4700:3033::ac43:9ad0) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256
* ALPN: server accepted h2
*  subject: CN=phamphutai.com
*  issuer: C=US; O=Google Trust Services; CN=WE1
*  SSL certificate verify ok.
> GET / HTTP/2
< HTTP/2 200
```

Every TLS 1.3 message from the section above is here, in order. Note that curl connected over **IPv6** and ALPN settled on **HTTP/2**.

### 4. Timing: `curl -w`

```
$ curl -s -o /dev/null -w "dns:%{time_namelookup} connect:%{time_connect} \
  tls:%{time_appconnect} ttfb:%{time_starttransfer} total:%{time_total}\n" \
  https://phamphutai.com/

dns:0.003148 connect:0.052460 tls:0.115769 ttfb:0.201334 total:0.207054
```

Each number is seconds since the start, so the steps are the gaps between them:

| Step | Time | Why |
|---|---|---|
| DNS | 3 ms | Answer already cached on the machine |
| TCP | ~49 ms | One round trip to Singapore |
| TLS | ~63 ms | One more round trip, plus crypto |
| Request to first byte | ~86 ms | Request travels, edge serves from cache |
| Download | ~6 ms | 5 KB of Brotli-compressed HTML |

Two more runs gave 0.28 seconds total, mostly a slower first byte. **Notice that the network round trips cost more than the server work.** That's why CDNs move content closer instead of making servers faster.

### 5. The browser: DevTools Network tab

Open any page, press F12 (or Cmd+Option+I on a Mac), open the **Network** tab and reload. Each row is one request: the HTML, then the CSS, scripts, fonts and images it pulls in. Click a row and open **Timing** to see the same phases as the `curl -w` breakdown: DNS lookup, initial connection, SSL, waiting for the server, download. The **Protocol** column (right-click the header row to add it) shows `h2` or `h3`. Tick **Disable cache** to see a first visit. The **Performance** tab shows the rendering steps and LCP.

---

## Key ideas

- **Opening a page is a chain:** URL, DNS, TCP, TLS, HTTP, edge, render. Each link can be slow, and each can break.
- **DNS is a cached tree.** Root, TLD and authoritative servers answer once; TTLs decide how long everyone else reuses the answer.
- **Round trips are the real cost.** TCP needs one, TLS 1.3 needs one, and QUIC folds both into one. Distance sets the price of each.
- **CDNs win by being close.** Anycast routes you to a nearby data centre; caching means the origin often isn't touched at all.
- **Headers are performance settings.** `cache-control`, `etag` and `content-encoding` decide whether bytes are sent at all, and how many.
- **The browser has its own critical path.** CSS blocks painting, plain scripts block parsing, and LCP, INP and CLS measure what users feel.
- **You can see all of it.** `dig`, `curl -sv`, `curl -w` and the DevTools Network tab turn the invisible steps into numbers.

---

*Sources: [RFC 6797: HSTS](https://www.rfc-editor.org/rfc/rfc6797) · [HSTS preload list](https://hstspreload.org/) · [Root server system](https://root-servers.org/) · [RFC 8484: DNS over HTTPS](https://www.rfc-editor.org/rfc/rfc8484) · [RFC 9293: TCP](https://www.rfc-editor.org/rfc/rfc9293) · [RFC 8446: TLS 1.3](https://www.rfc-editor.org/rfc/rfc8446) · [RFC 9113: HTTP/2](https://www.rfc-editor.org/rfc/rfc9113) · [RFC 9000: QUIC](https://www.rfc-editor.org/rfc/rfc9000) · [RFC 9114: HTTP/3](https://www.rfc-editor.org/rfc/rfc9114) · [Cloudflare: network](https://www.cloudflare.com/network/) · [Cloudflare: what is anycast](https://www.cloudflare.com/learning/cdn/glossary/anycast-network/) · [Cloudflare: HTTP headers (cf-ray)](https://developers.cloudflare.com/fundamentals/reference/http-headers/) · [Cloudflare Workers: static assets routing](https://developers.cloudflare.com/workers/static-assets/routing/worker-script/) · [MDN: HTTP caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching) · [web.dev: critical rendering path](https://web.dev/articles/critical-rendering-path) · [web.dev: Web Vitals](https://web.dev/articles/vitals) · [web.dev: LCP](https://web.dev/articles/lcp) · [web.dev: INP](https://web.dev/articles/inp) · [web.dev: CLS](https://web.dev/articles/cls) · [Meta: details about the October 4 outage](https://engineering.fb.com/2021/10/05/networking-traffic/outage-details/) · [Cloudflare: understanding how Facebook disappeared](https://blog.cloudflare.com/october-2021-facebook-outage/) · [Cloudflare: 18 November 2025 outage](https://blog.cloudflare.com/18-november-2025-outage/) · [Let's Encrypt: DST Root CA X3 expiration](https://letsencrypt.org/docs/dst-root-ca-x3-expiration-september-2021/)*

*Related: [Background Jobs Done Right](https://phamphutai.com/2026/10/14/background-jobs-done-right/) · [Run AI Models on Your Own Laptop](https://phamphutai.com/2026/11/04/run-ai-models-on-your-laptop/) · [Shipping Is Half the Job](https://phamphutai.com/2026/10/22/shipping-is-half-the-job/)*
