My page takes two seconds, and I can’t say where the time goes
See the problem
Section titled “See the problem”You built a small to-do app. On your laptop it opens at once. Your friend in Sydney opens it from a server in Virginia, and it takes almost two seconds.
The page is small: one HTML file and 30 files for images, styles and scripts. The server answers each one in a few milliseconds. So where do the two seconds go?
A second worry: the login form. Is the password safe on café Wi-Fi?
Define the task
Section titled “Define the task”- What you have: a browser (the client) and a web server far away. One round trip, a message there and back, takes 100 ms.
- What you want: the page and its 30 files loaded fast, and nobody on the way able to read or change them.
- The hard part: the time is not spent in your code. It is spent waiting for messages to cross the world, and each kind of setup costs extra round trips.
Pause and think
Section titled “Pause and think”Hint 1 of 3
A request travels inside a connection. Opening a connection takes a message there and back.
Hint 2 of 3
If every file gets its own connection, every file pays that cost again.
Hint 3 of 3
What if the browser kept one connection open and sent many requests on it?
Understand the idea
Section titled “Understand the idea”Think of ordering food by phone. You dial and say hello (open a connection). Then you order (the request), and they read it back (the response). If you hang up after every dish and dial again, most of the call is dialling.
The web works this way. A client (a browser or a phone app) sends a request. A server sends back a response. The language they use is HTTP.
A request names a method (what to do), a path (what to do it to), and headers (extra facts, like the content type). A response starts with a status code:
GET /todos HTTP/1.1 ← method, path, versionHost: todo.example.com ← headersAccept: application/json
HTTP/1.1 200 OK ← status codeContent-Type: application/jsonContent-Length: 31
[{"id": 1, "text": "Buy milk"}] ← body| Method | Means | Status codes you will use most |
|---|---|---|
GET | read something | 200 OK, 304 Not Modified |
POST | create something | 201 Created, 400 Bad Request |
PUT / PATCH | replace / change something | 200 OK, 204 No Content |
DELETE | remove something | 204 No Content, 404 Not Found |
| any | not signed in / not allowed | 401 Unauthorized, 403 Forbidden |
| any | the server failed | 500 Internal Server Error, 503 Service Unavailable |
The rule for the first digit: 2xx worked, 3xx look elsewhere, 4xx the client made a mistake, 5xx the server did. Here is a tiny real server, run by the tests on this machine:
"""A tiny to-do API: a client sends an HTTP request, the server sends a response.
GET /todos lists the to-dos. POST /todos adds one. Anything else gets an errorstatus, so the client knows what went wrong without reading the body."""
import jsonfrom http.server import BaseHTTPRequestHandler, HTTPServer
TODOS: list[dict[str, object]] = [{"id": 1, "text": "Buy milk"}]
class TodoHandler(BaseHTTPRequestHandler): def send_json(self, status: int, body: object) -> None: data = json.dumps(body).encode() self.send_response(status) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(data))) self.end_headers() self.wfile.write(data)
def do_GET(self) -> None: if self.path == "/todos": self.send_json(200, TODOS) else: self.send_json(404, {"error": "not found"})
def do_POST(self) -> None: if self.path != "/todos": self.send_json(404, {"error": "not found"}) return length = int(self.headers.get("Content-Length", 0)) try: item = json.loads(self.rfile.read(length)) text = item["text"] except (ValueError, KeyError, TypeError): self.send_json(400, {"error": 'send JSON like {"text": "..."}'}) return todo = {"id": len(TODOS) + 1, "text": text} TODOS.append(todo) self.send_json(201, todo)
def log_message(self, format: str, *args: object) -> None: # Keep test output quiet. pass
def make_server(port: int = 0) -> HTTPServer: """Port 0 lets the system pick a free port.""" return HTTPServer(("127.0.0.1", port), TodoHandler)HTTPS is HTTP inside TLS (Transport Layer Security). TLS scrambles the bytes so only the two ends can read them. The server also shows a certificate, signed by an authority your browser trusts, which proves it is the real site.
Watch the page load three ways. The player asks what happens next. Change the round trip time or the number of files below it.
v1: plain HTTP
Parts:
- Browser
- Web server
Arrows:
- Browser sends requests to Web server
Step 1 of 8. A page with 30 files (images, CSS, scripts). One round trip, a message to the server and back, takes 100 ms here.
The sums come from this model, which the tests check:
"""How many round trips a page takes to load, for the three ways of fetching it.
A round trip is one message to the server and its answer. Each way:- http: plain HTTP/1.1, a new connection for every file, 6 at a time.- https: the same, but every new connection also does a TLS handshake.- https_reused: HTTPS over one connection that is kept open (HTTP/2), so all the files are asked for at once."""
from math import ceil
# Browsers open at most 6 connections to one site over HTTP/1.1.PARALLEL = 6TCP = 1 # Round trips to open a TCP connection.TLS = 1 # Round trips for a TLS 1.3 handshake (TLS 1.2 took 2).ASK = 1 # Round trips to ask for one file and get it back.
def round_trips(files: int, way: str) -> int: """Round trips to load the page and then `files` more files (images, CSS, scripts).""" if way == "http": per_file = TCP + ASK return per_file + ceil(files / PARALLEL) * per_file if way == "https": per_file = TCP + TLS + ASK return per_file + ceil(files / PARALLEL) * per_file if way == "https_reused": # One connection and one handshake; then the page, then every file at once. return TCP + TLS + ASK + (ASK if files > 0 else 0) raise ValueError(f"unknown way {way!r}")
def load_ms(files: int, way: str, rtt_ms: int) -> int: """Time to load the page when one round trip takes `rtt_ms` milliseconds.""" return round_trips(files, way) * rtt_msClimb the ladder
Section titled “Climb the ladder”Each rung loads the same page: one HTML file and 30 more files, with 100 ms for each round trip.
Plain HTTP, a new connection for each file
The idea: Open a TCP connection, send the request, read the response, close. Do it again for the next file, 6 at a time.
Opening a connection is one round trip (the TCP handshake). Asking for the file is a second. Browsers open up to 6 connections to one site at once, so 30 files take 5 rounds.
- Page load
12 round trips, 1.2 s- Weak spot
anyone on the way can read it- Why
- 2 round trips for the page, then 30 files in 5 rounds of 6, each round 2 round trips.
Where it breaks: the login form. Every byte crosses the café Wi-Fi, the internet provider and every router as readable text. Anyone on the way can read the password, or change the page to add their own script.
HTTPS, still a new connection for each file
The idea: Wrap every connection in TLS. Now nobody on the way can read or change the traffic.
The waste in Okay: it sends secrets as plain text. HTTPS fixes that. TLS 1.3 needs one round trip for its handshake; the older TLS 1.2 needed two.
- Page load
18 round trips, 1.8 s- Weak spot
slower than before- Why
- Each connection now costs TCP (1) + TLS (1) + the request (1): 3 round trips instead of 2.
Where it breaks: every new connection repeats both handshakes. For 31 files that is the same work 6 times over, and the page got slower, not faster.
HTTPS on one connection kept open (HTTP/2)
The idea: Open one connection, do the TLS handshake once, and send every request on it. HTTP/2 lets many requests share one connection at the same time.
The waste in Better: the handshakes ran once per connection. Now they run once per visit. HTTP/1.1 can keep a connection open too (keep-alive), but it sends one request at a time on it. HTTP/2 sends them all together.
- Page load
4 round trips, 0.4 s- Weak spot
one slow file can still hold the page- Why
- TCP (1) + TLS (1) + the page (1) + all 30 files at once (1).
When Best isn’t best: HTTP/2 runs on TCP, so one lost packet makes every file on that connection wait. HTTP/3 runs on QUIC (over UDP) to avoid that, and joins the two handshakes into one. For a small internal tool on one network, HTTP/1.1 with keep-alive is fine.
Draw it yourself
Section titled “Draw it yourself”Build the to-do app on the board. Drag parts onto it, press the + on a part to draw an arrow, and press Check my design. Step 2 adds a name people can type.
Step 1: A to-do list app
- People add, tick and delete to-dos in their browser.
- The to-dos must still be there tomorrow.
- About 50 users. Keep it simple.
Solid arrow: sends requests toDashed arrow: copies data to
Watch the traffic
Send 10 requests through your board and see where each one goes. Then make a part fail and send them again.
How requests move
Requests start at the Client and follow your arrows. Reads try a cache first (4 in 5 are found there), then a read replica, then the database. Writes go to the database. Background jobs go on a queue, and a worker saves them. A load balancer skips a part that is down; nothing else can.
Compare
Section titled “Compare”| Way | Round trips (31 files) | At 100 ms | Safe on public Wi-Fi | Use it when |
|---|---|---|---|---|
| HTTP, new connection each file | 12 | 1.2 s | No | Never on the public internet |
| HTTPS, new connection each file | 18 | 1.8 s | Yes | Old clients that can’t reuse connections |
| HTTPS, one connection (HTTP/2) | 4 | 0.4 s | Yes | Almost always: every modern browser supports it |
Common mistakes and edge cases
Section titled “Common mistakes and edge cases”- 401 or 403?
401 Unauthorizedmeans “I don’t know who you are: sign in”.403 Forbiddenmeans “I know who you are, and you may not do this”. - GET must not change data. Browsers, crawlers and caches may repeat a
GETor load it early. AGET /delete?id=5link can delete things by itself. - HTTP has no memory. Each request stands alone. A login is kept with a cookie the browser sends back on every request.
- HTTPS on part of the site. One HTTP page or script on an HTTPS site lets an attacker change it. Send everything over HTTPS, and tell browsers to do so with the
Strict-Transport-Securityheader. - The round trip, not the bandwidth. A faster connection does not help much when the time is spent waiting for answers. Fewer round trips, or a server closer to users, does.
Say it in the interview
Section titled “Say it in the interview”Questions to ask first
- Who are the clients: browsers, phone apps, or other services?
- Where are the users, and where are the servers?
- Is any of the data private, like passwords or payments?
A 60-second walkthrough
“The client sends an HTTP request: a method, a path and headers. The server answers with a status code, headers and a body. Over HTTPS, all of that is inside TLS, so nobody on the way can read or change it. The certificate proves the server is real. The cost is round trips: one to open a TCP connection, one for the TLS 1.3 handshake, then one per request. So I keep connections open and use HTTP/2, which sends many requests on one connection. For a page with 30 files at 100 ms a round trip, that is 0.4 seconds instead of 1.8.”
Likely follow-ups
- What happens when I type a URL and press Enter? DNS turns the name into an address, then TCP connects, TLS does its handshake, the browser sends
GET, and the server answers. Then the browser asks for the files the page names. - Why is HTTPS slower? It adds one round trip per new connection (two on TLS 1.2), and a little work to encrypt. Reusing connections hides most of it.
- PUT or PATCH?
PUTreplaces the whole thing;PATCHchanges some fields. - What is HTTP/3? HTTP over QUIC, which runs on UDP. One lost packet no longer stops every request, and it sets up the connection and encryption in one round trip.
Red flags interviewers notice
- Saying HTTPS only “hides the URL”. It hides the path, headers and body; the site name can still be seen.
- Using
GETfor actions that change data. - Returning
200for every error.
Practice
Section titled “Practice”1. Status codes for an API (easy)
Section titled “1. Status codes for an API (easy)”A shop API has these cases. Pick a status code for each: an order was created; the JSON was broken; the order id does not exist; the user is not signed in; the database is down.
Hint 1 of 3
Start with the first digit: did it work, did the client make a mistake, or did the server?
Hint 2 of 3
Creating something has its own success code, not plain 200.
Hint 3 of 3
Not signed in and not allowed are two different codes.
Show the answers
200 for success, 500 for everything else
The idea: Every success is 200 and every problem is 500.
A client sending broken JSON will retry forever, because 500 says “try again later”.
- Works for
any API- Weak spot
clients can’t tell their mistakes from yours- Why
- A broken request looks like a server crash, so monitoring alarms and clients retry a request that will never work.
The right family for each case
The idea: 2xx when it worked, 4xx when the client must change the request, 5xx when the server failed.
Created: 200. Broken JSON: 400. Missing order: 404. Not signed in: 400. Database down: 500.
- Works for
any API- Weak spot
some codes too general- Why
- Clients know whether retrying can help: never for 4xx, maybe later for 5xx.
The exact code for each case
The idea: 201 Created, 400 Bad Request, 404 Not Found, 401 Unauthorized, 503 Service Unavailable.
503 says the server is fine but a part it needs is down for now. Add Retry-After: 30 so clients wait before trying again.
- Works for
any API- Weak spot
none- Why
- 201 can carry a Location header with the new order’s address; 503 can carry Retry-After.
2. Slow on the other side of the world (medium)
Section titled “2. Slow on the other side of the world (medium)”Users in Sydney say your site takes 3 seconds. Users in Virginia, next to the server, say 0.2 seconds. The server answers each request in 10 ms. What is happening, and what do you change first?
Hint 1 of 3
A round trip from Sydney to Virginia takes about 200 ms. From Virginia, about 2 ms.
Hint 2 of 3
Count the round trips in a page load with a new connection for each file.
Hint 3 of 3
You can cut the round trips, or make each one shorter.
Show the answers
A bigger server
The idea: Buy a faster machine.
15 round trips at 200 ms is 3 seconds, and none of it is the server.
- Saves
nothing changes for Sydney- Weak spot
money spent on the wrong thing- Why
- The server spends 10 ms per request. The rest is distance.
Fewer round trips
The idea: Turn on HTTP/2 and keep connections open, so the handshakes happen once.
This is the cheapest fix: a setting on the web server, with no new machines.
- Saves
about 4 round trips, 0.8 s- Weak spot
every round trip still crosses the world- Why
- Most of the round trips were handshakes and one-at-a-time requests.
Shorter round trips too
The idea: Serve the files from a CDN near Sydney, and end TLS there, so most round trips are short.
The CDNs lesson shows how.
- Saves
well under 0.5 s- Weak spot
one more service to set up- Why
- A CDN server in Sydney is about 5 ms away, so handshakes and files are fast; only the dynamic data goes to Virginia.
3. A login form over HTTP (medium)
Section titled “3. A login form over HTTP (medium)”An old internal site has a login form on plain HTTP. The team says, “It’s fine, the password is hashed in the database.” Is it fine? What do you change?
Hint 1 of 3
Hashing protects the password where it is stored. What about on its way to the server?
Hint 2 of 3
After login, the browser sends a cookie with every request. What can someone do with it?
Hint 3 of 3
One HTTP page on the site is enough for an attacker to change what the user sees.
Show the answers
HTTPS on the login page only
The idea: Move the login form to HTTPS.
Anyone who reads the cookie can act as the user without the password.
- Protects
the password on its way in- Weak spot
the session cookie is still sent in plain text- Why
- Every later page sends the login cookie over HTTP, and the cookie is as good as the password.
HTTPS on every page
The idea: Serve the whole site over HTTPS, and send HTTP requests to HTTPS with a 301 redirect.
An attacker can answer that first request with a fake page.
- Protects
every request- Weak spot
the first HTTP request can still be changed- Why
- Someone who types the address without https:// sends one plain request first.
HTTPS everywhere, enforced
The idea: HTTPS on every page, the Strict-Transport-Security header, and cookies marked Secure and HttpOnly.
Certificates are free from services like Let’s Encrypt, so cost is no reason to skip HTTPS.
- Protects
every request, from the first- Weak spot
none worth naming- Why
- The browser remembers to use HTTPS only; Secure cookies are never sent over HTTP; HttpOnly hides them from scripts.
Check yourself
Section titled “Check yourself”Cheat sheet
Section titled “Cheat sheet”- A request is a method, a path, headers and maybe a body. A response is a status code, headers and a body.
- 2xx worked, 3xx look elsewhere, 4xx the client’s mistake, 5xx the server’s. 201 for created, 401 for not signed in, 403 for not allowed.
- Round trips: TCP takes 1, TLS 1.3 takes 1, each request takes 1. Reuse the connection.
- HTTPS hides the path, headers and body, and the certificate proves who the server is.
- HTTP/2 sends many requests on one connection; HTTP/3 does it over QUIC.
Go deeper
Section titled “Go deeper”- MDN: An overview of HTTP
- MDN: HTTP response status codes
- RFC 9110: HTTP Semantics
- Next: DNS, the step before the first request.
Can you do these now?
If you’re not sure of any of these, scroll back to that part before moving on.
Status: Not started