Skip to content

My page takes two seconds, and I can’t say where the time goes

  • System Design · Foundations
  • 15 min read, about 50 min with practice
  • Concept
  • Asked often: medium
  • Reviewed 2026-11

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?

  • 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.
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?

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, version
Host: todo.example.com ← headers
Accept: application/json
HTTP/1.1 200 OK ← status code
Content-Type: application/json
Content-Length: 31
[{"id": 1, "text": "Buy milk"}] ← body
MethodMeansStatus codes you will use most
GETread something200 OK, 304 Not Modified
POSTcreate something201 Created, 400 Bad Request
PUT / PATCHreplace / change something200 OK, 204 No Content
DELETEremove something204 No Content, 404 Not Found
anynot signed in / not allowed401 Unauthorized, 403 Forbidden
anythe server failed500 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 to-do API: requests in, status codes out
"""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 error
status, so the client knows what went wrong without reading the body.
"""
import json
from 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.

Loading a page: HTTP, HTTPS, and HTTPS on one connection

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.

Try your own input

The sums come from this model, which the tests check:

Round trips to load a page
"""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 = 6
TCP = 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_ms

Each rung loads the same page: one HTML file and 30 more files, with 100 ms for each round trip.

Okay

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.

Better

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.

Best

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.

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.
Add parts from the list above. Then press the + on a part to draw an arrow.

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.

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

WayRound trips (31 files)At 100 msSafe on public Wi-FiUse it when
HTTP, new connection each file121.2 sNoNever on the public internet
HTTPS, new connection each file181.8 sYesOld clients that can’t reuse connections
HTTPS, one connection (HTTP/2)40.4 sYesAlmost always: every modern browser supports it
  • 401 or 403? 401 Unauthorized means “I don’t know who you are: sign in”. 403 Forbidden means “I know who you are, and you may not do this”.
  • GET must not change data. Browsers, crawlers and caches may repeat a GET or load it early. A GET /delete?id=5 link 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-Security header.
  • 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.

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? PUT replaces the whole thing; PATCH changes 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 GET for actions that change data.
  • Returning 200 for every error.

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
Okay

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

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

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
Okay

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

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

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.

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
Okay

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

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

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.
  1. Multiple choiceA client sends POST /orders with broken JSON. Which status code fits?
  2. Fill in the answerA browser opens a new HTTPS connection (TLS 1.3) to ask for one file. How many round trips before the file arrives? (Type a number.)
  3. Multiple choiceA page has 30 files and each round trip takes 100 ms. Which change helps most?
  4. Spot the bugWhat is wrong with this handler?
    def get_order(order_id):
        order = db.find(order_id)
        if order is None:
            return 200, {"error": "not found"}
        return 200, order
  5. Multiple choiceThe user is signed in, but tries to delete someone else’s post. Which code?
  • 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.

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