Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Computer Networks  Module 1 – How the Internet works

What happens when you load a web page

Follow one real page load through DNS, TCP or QUIC, TLS 1.3 and HTTP, with measured timings that show why a first visit is slower than the next one.

  • Beginner
  • 25 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free: 25 minutes

What you will learn

  • Trace one page load through DNS, a transport connection, TLS and HTTP using recorded command output
  • Name the identifier used at each step (MAC address, IP address, port, domain name) and the layer that uses it
  • Explain why a first visit is slower than a repeat visit, using measured phase timings
What you’ll learn

What you will learn

  • Trace one page load through DNS, a transport connection, TLS and HTTP using recorded command output
  • Name the identifier used at each step (MAC address, IP address, port, domain name) and the layer that uses it
  • Explain why a first visit is slower than a repeat visit, using measured phase timings
On this page

When you open https://www.python.org/, the browser cannot just ask for the page. First it turns the name into an IP address with the Domain Name System (DNS). Then it sends packets towards that address through your router, opens a transport connection (TCP, or QUIC for HTTP/3), and runs a TLS 1.3 handshake to agree on encryption keys and check the server’s certificate. Only then does the HTTP request leave. Almost every one of these steps has to wait for an answer from the other side, so the time until the first byte of the page arrives is mostly a count of network round trips multiplied by how far away the server is.

This lesson follows one real page load step by step, with each command’s output recorded on a real computer, and then measures why the second visit is so much faster than the first.

The steps at a glance

On a first visit over TCP, five things happen before the page starts to arrive. The diagram shows the messages of the steps that cross the network, and the list below it describes all five.

A first HTTPS visit as four round trips: a DNS lookup, the TCP handshake, the TLS 1.3 handshake and the HTTP request.Round trip 1: DNSRound trip 2: TCP handshakeRound trip 3: TLS 1.3 handshakeRound trip 4: the requestBrowser: www.python.org, address?Resolver: CNAME to the CDN, then an A recordBrowser: SYNServer: SYN-ACKBrowser: ACK + ClientHelloServer: ServerHello ... FinishedBrowser: Finished + GET /Server: HTTP 200 and the first bytes

The four round trips of a first visit over TCP and TLS 1.3

Text description of the diagram

The diagram shows four boxes from top to bottom, one for each round trip, each holding a message and its answer.

  1. Round trip 1, DNS: the browser asks the resolver for the address of www.python.org, and the resolver answers with a CNAME that points to the content delivery network (CDN), then an A record with an IPv4 address.
  2. Round trip 2, the TCP handshake: the browser sends SYN to the server, and the server answers with SYN-ACK.
  3. Round trip 3, the TLS 1.3 handshake: the browser sends the final TCP ACK together with the TLS ClientHello, and the server answers with its ServerHello, its certificate and its Finished message.
  4. Round trip 4, the request: the browser sends its TLS Finished message with the HTTP request GET /, and the server answers with the HTTP 200 status line, the headers and the first bytes of the page.
  1. Resolve the name with DNS: www.python.org becomes an IP address. That costs about one round trip to your resolver, and more when its cache has no answer.
  2. Choose the first hop: the routing table says “send it to the router”, and ARP (IPv4) or Neighbor Discovery (IPv6) finds the router’s hardware address. Usually no round trip at all, because your computer already knows its router.
  3. Open a connection with TCP: SYN, SYN-ACK, ACK, one round trip.
  4. Agree on keys with TLS 1.3: ClientHello, then ServerHello to Finished, and the certificate is checked. One round trip; QUIC does steps 3 and 4 together, in one.
  5. Ask for the page with HTTP/2 or HTTP/3: GET /, then the status line, headers and body. One round trip, plus the time the server takes.

Steps 1 to 5 cost about four round trips, and the request itself is only the last of them. The rest of the lesson looks at each step and then measures them.

Every layer has its own kind of address

Each step uses its own identifier, and each identifier is valid in a different place. Keeping them apart makes every later topic of this track easier.

  • Domain name, such as www.python.org: valid everywhere, since it is the name people type. DNS turns it into addresses, TLS checks it against the server’s certificate, and HTTP sends it so that one server can host many sites.
  • IP address, such as 167.82.24.223: valid from end to end, across every network on the path, although network address translation (NAT) may rewrite it on the way. Every router forwards a packet by its destination address.
  • Port, such as 443: valid on one host, where it picks the program, here the HTTPS server. TCP and UDP carry it.
  • MAC address, such as 00:00:5e:00:53:01, an address reserved for documentation: valid on one link only, such as your Wi-Fi network. Ethernet and Wi-Fi frames carry it.

The MAC address explains step 2. A frame that leaves your laptop carries the server’s IP address but the router’s MAC address, because the router is the only machine on your link that will pass it on. Your computer learns that MAC address with ARP for IPv4 (RFC 826) or Neighbor Discovery for IPv6 (RFC 4861), and keeps it in a cache. Each router then takes the packet out of the frame and puts it in a new frame for the next link, with new MAC addresses; the IP addresses inside stay the same.

Step 1: turning the name into addresses

DNS answers carry a time to live (TTL) in seconds, and every resolver along the way may keep the answer for that long (RFC 1034). Here is the same question asked twice, five seconds apart, of a public resolver at 8.8.8.8 with dig:

Two lookups of www.python.org, five seconds apart (recorded) Text · dig_runs.txt
$ dig @8.8.8.8 www.python.org A +noall +answer +stats

; <<>> DiG 9.10.6 <<>> @8.8.8.8 www.python.org A +noall +answer +stats
; (1 server found)
;; global options: +cmd
www.python.org.		18576	IN	CNAME	dualstack.python.map.fastly.net.
dualstack.python.map.fastly.net. 60 IN	A	167.82.24.223
;; Query time: 39 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; MSG SIZE  rcvd: 104

$ dig @8.8.8.8 www.python.org A +noall +answer +stats

; <<>> DiG 9.10.6 <<>> @8.8.8.8 www.python.org A +noall +answer +stats
; (1 server found)
;; global options: +cmd
www.python.org.		18570	IN	CNAME	dualstack.python.map.fastly.net.
dualstack.python.map.fastly.net. 54 IN	A	167.82.24.223
;; Query time: 16 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; MSG SIZE  rcvd: 104

Three things are worth reading in that output:

  • The name is an alias. www.python.org is a CNAME (a canonical-name record) for a name of the site’s content delivery network (CDN), and only that name has an A record with an IPv4 address. Many large sites hand their traffic to a CDN this way.
  • The TTLs count down. The first answer gave the A record a TTL of 60 seconds; five seconds later the same resolver answered with 54, so the second answer came from its cache, and the first had most likely just been fetched.
  • The cache was faster. The query time fell from 39 ms to 16 ms. A lookup that misses every cache can take much longer, because the resolver must ask other servers first; the DNS module of this track follows that path.

Your browser and your operating system keep caches of their own, so a name you used a moment ago is usually answered without asking the network at all.

About these recordings

Every recorded output in this lesson comes from one Mac (macOS 26.7.1 on Apple silicon) on a home fibre connection in India, made with dig 9.10.6, Homebrew curl 8.22.0 and OpenSSL 4.0.3. The lines that showed the date and time of a run are left out. Your numbers will be different, because they depend on where you are and on your network.

Steps 3 and 4: an encrypted connection

TCP opens a connection with a three-way handshake: the client sends SYN, the server replies SYN-ACK, and the client sends ACK (RFC 9293). That costs one round trip before any data can move. The client’s ACK can travel together with its first data, which for HTTPS is the TLS ClientHello.

TLS 1.3 then needs one more round trip (RFC 9846). The ClientHello already carries the client’s key share and the name of the site it wants (the server name indication), so the server can answer with its own key share, its certificate and its Finished message in one flight. After that reply the client checks the certificate, sends its Finished message and can send the HTTP request with it.

openssl s_client shows what a handshake agreed on:

TLS handshakes with two servers (recorded) Text · tls_handshakes.txt
$ openssl s_client -brief -connect www.python.org:443 -servername www.python.org < /dev/null
Connecting to 167.82.24.223
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_128_GCM_SHA256
Peer certificate: CN=www.python.org
Hash used: SHA256
Signature type: rsa_pss_rsae_sha256
Verification: OK
Negotiated TLS1.3 group: X25519MLKEM768
DONE
$ openssl s_client -brief -connect www.gnu.org:443 -servername www.gnu.org < /dev/null
Connecting to 209.51.188.116
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=wildebeest1p.gnu.org
Hash used: SHA256
Signature type: rsa_pss_rsae_sha256
Verification: OK
Peer Temp Key: X25519, 253 bits
DONE

Both servers chose TLS 1.3 with AES in GCM mode, and both certificates verified. They differ in the key exchange. www.gnu.org used X25519, a classical elliptic-curve exchange. The CDN in front of www.python.org used X25519MLKEM768, which combines X25519 with ML-KEM-768, a post-quantum key encapsulation. The shared secret stays safe as long as either of the two holds, which protects recorded traffic against a future quantum computer able to break X25519 (RFC 10024). The number of round trips is the same either way.

QUIC (RFC 9000) runs over UDP and builds the TLS 1.3 handshake into its own connection setup: where TCP and TLS take two round trips before the request can go, QUIC takes one. HTTP/3 is HTTP carried over QUIC (RFC 9114).

Step 5: the request, and who answers it

The request names the method and the resource (GET /) and the site, and the response starts with a status code and headers (RFC 9110). A few of the response’s headers show who actually answered:

Selected response headers from www.python.org (recorded) Text · python_org_headers.txt
$ curl -sI https://www.python.org/
HTTP/2 200
content-type: text/html; charset=utf-8
server: nginx
via: 1.1 varnish, 1.1 varnish, 1.1 varnish
age: 2244
x-served-by: cache-iad-kcgs7200042-IAD, cache-iad-kcgs7200042-IAD, cache-iad-kcgs7270046-IAD, cache-bom-vanm7210087-BOM
x-cache: MISS, HIT, HIT
x-cache-hits: 0, 4, 25
strict-transport-security: max-age=63072000; includeSubDomains; preload
content-length: 51995
  • x-served-by lists the cache servers that handled the response; in this CDN’s naming each entry ends with the code of its data centre (Fastly). The last one ends in BOM, its data centre in Mumbai (POP list), and the others in IAD, near Washington.
  • x-cache ends with HIT: the cache that answered, the last one in the list, already had a copy and did not need to ask the origin server.
  • age: 2244 says the cached copy was about 37 minutes old (RFC 9111).

So the browser did not talk to the server where python.org’s pages are made. It talked to a CDN cache a few milliseconds away, which had a copy. That is normal for popular sites: a CDN edge, a load balancer or a reverse proxy usually ends your TCP and TLS connection, and the origin may be on another continent.

Measuring a real page load

curl can print when each phase of a request finished, counted in seconds from the start of the request (curl manual): time_namelookup when the name was resolved, time_connect when the TCP connection was open, time_appconnect when the TLS handshake was done (for HTTP/3, when the QUIC connection with its built-in TLS was ready), and time_starttransfer when the first byte arrived. num_connects says how many new connections the request needed. The program below reads a recorded session of six requests and turns those moments into phases:

Phases and round trips of six recorded requests Python · round_trips.py
"""Turns curl's recorded timings into the phases of a page load, then counts round trips.

Each curl -w variable is a moment, measured from the start of the transfer:
  namelookup     the name has been resolved to an address (DNS)
  connect        the TCP connection is open
  appconnect     the TLS handshake is finished (for HTTP/3: the QUIC connection, which includes TLS, is ready)
  starttransfer  the first byte of the response has arrived
The input is a recorded session: each "$ curl ..." line is followed by the line(s) it printed.
"""
import sys

runs = []
for line in sys.stdin:
    line = line.strip()
    if line.startswith("$ curl"):
        host = line.split()[-1].split("/")[2]  # https://www.python.org/ -> www.python.org
        http3 = "--http3-only" in line
    elif line.startswith("http="):
        fields = dict(field.split("=") for field in line.split())
        t = {name: float(value) * 1000 for name, value in fields.items() if name != "http"}
        reused = fields["new"] == "0"  # curl opened no new connection for this request
        runs.append({
            "run": host + (", same connection" if reused else ", HTTP/3" if http3 else ""),
            "tcp": not reused and not http3,
            "dns": t["namelookup"],
            "handshakes": t["appconnect"] - t["namelookup"] if not reused else 0.0,
            "tcp_handshake": t["connect"] - t["namelookup"],
            "tls_handshake": t["appconnect"] - t["connect"],
            "first_byte": t["starttransfer"] - t["appconnect"],
            "total": t["total"],
        })

print(f"{'run (milliseconds)':32} {'dns':>6} {'handshakes':>11} {'to 1st byte':>12} {'total':>8}")
for r in runs:
    print(f"{r['run']:32} {r['dns']:6.1f} {r['handshakes']:11.1f} {r['first_byte']:12.1f} {r['total']:8.1f}")

print()
print("A TCP handshake is one round trip, so connect - namelookup estimates the round-trip time (RTT).")
print(f"{'run over TCP':32} {'RTT ms':>7} {'TLS in RTTs':>12} {'to 1st byte in RTTs':>20}")
for r in runs:
    if r["tcp"]:
        rtt = r["tcp_handshake"]
        print(f"{r['run']:32} {rtt:7.1f} {r['tls_handshake'] / rtt:12.1f} {r['first_byte'] / rtt:20.1f}")

Input (standard input)

$ W='http=%{http_version} new=%{num_connects} namelookup=%{time_namelookup} connect=%{time_connect} appconnect=%{time_appconnect} starttransfer=%{time_starttransfer} total=%{time_total}\n'
$ curl -s -o /dev/null -w "$W" https://www.python.org/
http=2 new=1 namelookup=0.003606 connect=0.014310 appconnect=0.032947 starttransfer=0.051835 total=0.064813
$ curl -s -o /dev/null -w "$W" https://www.python.org/
http=2 new=1 namelookup=0.003525 connect=0.012868 appconnect=0.026678 starttransfer=0.043239 total=0.054140
$ curl -s -o /dev/null -o /dev/null -w "$W" https://www.python.org/ https://www.python.org/
http=2 new=1 namelookup=0.003212 connect=0.014600 appconnect=0.030988 starttransfer=0.048812 total=0.061206
http=2 new=0 namelookup=0.000000 connect=0.000000 appconnect=0.000000 starttransfer=0.011838 total=0.012697
$ curl --http3-only -s -o /dev/null -w "$W" https://www.python.org/
http=3 new=1 namelookup=0.003159 connect=0.015318 appconnect=0.020182 starttransfer=0.035968 total=0.054281
$ curl -s -o /dev/null -w "$W" https://www.gnu.org/
http=1.1 new=1 namelookup=0.006509 connect=0.247481 appconnect=0.500135 starttransfer=0.758298 total=1.225121

Output

run (milliseconds)                  dns  handshakes  to 1st byte    total
www.python.org                      3.6        29.3         18.9     64.8
www.python.org                      3.5        23.2         16.6     54.1
www.python.org                      3.2        27.8         17.8     61.2
www.python.org, same connection     0.0         0.0         11.8     12.7
www.python.org, HTTP/3              3.2        17.0         15.8     54.3
www.gnu.org                         6.5       493.6        258.2   1225.1

A TCP handshake is one round trip, so connect - namelookup estimates the round-trip time (RTT).
run over TCP                      RTT ms  TLS in RTTs  to 1st byte in RTTs
www.python.org                      10.7          1.7                  1.8
www.python.org                       9.3          1.5                  1.8
www.python.org                      11.4          1.4                  1.6
www.gnu.org                        241.0          1.0                  1.1

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 round_trips.py

Read the two tables together:

  • A nearby edge. For www.python.org the TCP handshake took 9 to 11 ms, so the CDN’s edge is about one 10 ms round trip away. The TLS handshake took 1.4 to 1.7 round trips, most likely because computing the keys and sending the certificate add a few milliseconds, which shows when the distance is this short.
  • The same connection again. When curl sent a second request on the connection it had just opened, the name lookup and both handshakes cost nothing (new=0), and the first byte came back in 11.8 ms, about one round trip.
  • HTTP/3. The QUIC connection, with TLS built in, was ready 17.0 ms after the name lookup, against 23 to 29 ms for TCP and TLS: about one round trip saved.
  • A distant server. www.gnu.org answered from a server 241 ms away. Here the TLS handshake took almost exactly one round trip and the first byte one more, so the first byte arrived 758 ms after the start. The whole page took 1.2 seconds, because sending a larger body takes more round trips, as the TCP module explains.

The pattern is the same everywhere: when the server is close, computing and the server’s own work show up; when it is far, the number of round trips decides almost everything.

A model: round trips decide the first byte

That pattern can be written as a formula. With the DNS answer cached, the first byte arrives after the handshake round trips, plus one round trip for the request, plus the server’s own time. This model compares four kinds of connection at three distances:

Time to first byte by connection type and distance Python · ttfb_model.py
"""A model of the time to first byte (TTFB) of an HTTPS request whose DNS answer is already cached.

Every handshake costs whole round trips before the request can leave, then the request and
the start of the response cost one more round trip, plus the time the server needs to answer.
"""
SERVER_MS = 20  # an assumption: the server needs 20 ms to start its response

HANDSHAKE_ROUND_TRIPS = {
    "TCP + TLS 1.2": 3,  # TCP's handshake, then TLS 1.2's full handshake of two round trips
    "TCP + TLS 1.3": 2,  # TCP's handshake, then TLS 1.3's one round trip
    "QUIC, new connection": 1,  # QUIC sets up the transport and TLS 1.3 together
    "QUIC, 0-RTT resumption": 0,  # the request travels in the very first flight
}
RTTS_MS = (30, 85, 250)  # a nearby edge, another region, another continent


def ttfb(handshakes, rtt):
    return (handshakes + 1) * rtt + SERVER_MS


print(f"{'TTFB (ms) at a round-trip time of:':34}" + "".join(f"{f'{rtt} ms':>9}" for rtt in RTTS_MS))
for setup, handshakes in HANDSHAKE_ROUND_TRIPS.items():
    print(f"{setup:34}" + "".join(f"{ttfb(handshakes, rtt):>9}" for rtt in RTTS_MS))

tls13 = HANDSHAKE_ROUND_TRIPS["TCP + TLS 1.3"]
quic = HANDSHAKE_ROUND_TRIPS["QUIC, new connection"]
print()
print(f"On the 250 ms path, QUIC instead of TCP + TLS 1.3 saves {ttfb(tls13, 250) - ttfb(quic, 250)} ms.")
print(f"Serving from a 30 ms edge instead saves {ttfb(tls13, 250) - ttfb(tls13, 30)} ms, with no protocol change.")

Output

TTFB (ms) at a round-trip time of:    30 ms    85 ms   250 ms
TCP + TLS 1.2                           140      360     1020
TCP + TLS 1.3                           110      275      770
QUIC, new connection                     80      190      520
QUIC, 0-RTT resumption                   50      105      270

On the 250 ms path, QUIC instead of TCP + TLS 1.3 saves 250 ms.
Serving from a 30 ms edge instead saves 660 ms, with no protocol change.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ttfb_model.py

Two results stand out. Each handshake round trip is worth a lot on a long path: on a 250 ms path, QUIC’s single handshake saves 250 ms. But moving the server closer saves more than any protocol change, which is why popular sites pay for CDNs.

The last row needs care. With 0-RTT resumption, a client that has talked to the server before sends its request in the very first flight, encrypted with a key from the earlier connection. TLS cannot stop an attacker from replaying such early data, so a client must only send early data that is safe to repeat (RFC 9846, section 8), and a server can refuse an early request with the status 425 Too Early (RFC 8470). Fetching a page is fine; placing an order is not.

Why the second visit is faster

A repeat visit skips work at almost every step:

  • DNS answers are cached by the browser, the operating system and the resolver until their TTL runs out.
  • Connections are reused. HTTP/1.1 keeps a connection open for further requests (RFC 9112), and HTTP/2 and HTTP/3 send many requests over one connection at the same time, so the handshakes are paid once per site, not once per file. The same connection row above shows the saving.
  • TLS sessions resume. After a handshake, the server can give the client a ticket for a later connection, which shortens the next handshake and makes 0-RTT possible.
  • HTTP caching can remove the request entirely. A response with Cache-Control: max-age=3600 may be reused for an hour without asking the server; after that, the browser can ask whether its copy is still valid and receive a short 304 Not Modified instead of the whole file (RFC 9111).

Your browser’s developer tools show the same phases for every request of a page, and can save them as a HAR file.

HAR File Analyzer & Sanitizer Load a HAR file saved from your browser's developer tools to see the DNS, connection, TLS and waiting time of every request. DNS Lookup Look up the A, AAAA and CNAME records of any site and see whether it sits behind a CDN. SSL Certificate Checker See which certificate a site presents in its TLS handshake and when it expires.

Interview questions

Fresher · Name the steps between entering an HTTPS address in a browser and receiving the first byte of the page.

The browser resolves the domain name to an IP address with DNS, usually from a cache. It sends its packets to the default router, whose hardware address it knows from ARP or Neighbor Discovery. It opens a TCP connection with a three-way handshake (one round trip), then runs the TLS 1.3 handshake (one more round trip), checking that the server’s certificate matches the name. Only then does it send the HTTP request, and the first byte arrives about one round trip later, plus the server’s time. With HTTP/3, QUIC combines the transport and TLS handshakes into one round trip. Mention that the server answering is often a CDN edge rather than the origin.

Key takeaways

  • A first HTTPS visit over TCP costs about four round trips before the first byte: DNS, TCP, TLS and the request. QUIC removes one, and 0-RTT resumption one more, for requests that are safe to repeat.
  • Each layer has its own address: the domain name for people, the IP address from end to end, the port for the program and the MAC address for one link only.
  • Distance multiplies every round trip. In the recordings, the same steps took 52 ms to a CDN edge about 10 ms away and 758 ms to an origin 241 ms away.
  • Repeat visits are faster because DNS answers, connections, TLS sessions and responses are all cached or reused.
  • The server you reach is often a CDN edge or a proxy; headers such as age, x-cache or via tell you so.

Exercise

Exercise · Easy · Python

Split curl's timings into the phases of a page load

The lesson's curl command prints one line per request, such as:

http=2 new=1 namelookup=0.003606 connect=0.014310 appconnect=0.032947 starttransfer=0.051835 total=0.064813

Each value is a moment in seconds, counted from the start of the request. Write two functions in phases.py.

phases(line) returns how long each phase took, in milliseconds, as a dictionary with four keys:

  • "dns": from the start until the name was resolved (namelookup);
  • "connect": from then until the transport connection was open (connect);
  • "tls": from then until the TLS handshake was done (appconnect);
  • "wait": from then until the first byte of the response arrived (starttransfer).

Two cases need care. A plain http:// request has no TLS, and curl prints appconnect=0.000000: its "tls" phase is 0 and its wait starts when the connection opens. A request on a reused connection prints zeros for the first three moments, so only its wait is more than 0.

dominant(durations) takes such a dictionary and returns the key of the longest phase, for example "wait".

The sample tests use lines recorded in the lesson and accept answers within 1 ms.

Starter code · phases.py

def phases(line):
    """Split one line printed by curl -w into phase durations in milliseconds.

    Returns a dictionary with the keys "dns", "connect", "tls" and "wait".
    """
    # Replace this line with your code.
    return {"dns": 0.0, "connect": 0.0, "tls": 0.0, "wait": 0.0}


def dominant(durations):
    """The key of the longest phase, such as "wait"."""
    # Replace this line with your code.
    return ""
The sample tests · test_phases.py
import math

from phases import dominant, phases

FIRST_VISIT = "http=2 new=1 namelookup=0.003606 connect=0.014310 appconnect=0.032947 starttransfer=0.051835 total=0.064813"
FAR_ORIGIN = "http=1.1 new=1 namelookup=0.006509 connect=0.247481 appconnect=0.500135 starttransfer=0.758298 total=1.225121"
SLOW_SERVER = "http=2 new=1 namelookup=0.028254 connect=0.036660 appconnect=0.054341 starttransfer=0.349055 total=0.351304"
PLAIN_HTTP = "http=1.1 new=1 namelookup=0.018120 connect=0.257654 appconnect=0.000000 starttransfer=0.515179 total=0.758801"
REUSED = "http=2 new=0 namelookup=0.000000 connect=0.000000 appconnect=0.000000 starttransfer=0.011838 total=0.012697"


def close(actual, expected):
    """True when every phase is within 1 ms of the expected value."""
    return set(actual) == set(expected) and all(math.isclose(actual[k], expected[k], abs_tol=1.0) for k in expected)


def test_first_visit():
    """splits a first visit to a nearby CDN edge"""
    assert close(phases(FIRST_VISIT), {"dns": 3.6, "connect": 10.7, "tls": 18.6, "wait": 18.9})


def test_far_origin():
    """splits a visit to a server on another continent"""
    assert close(phases(FAR_ORIGIN), {"dns": 6.5, "connect": 241.0, "tls": 252.7, "wait": 258.2})


def test_slow_server():
    """splits a request whose server took almost 0.3 s to answer"""
    assert close(phases(SLOW_SERVER), {"dns": 28.3, "connect": 8.4, "tls": 17.7, "wait": 294.7})


def test_plain_http():
    """gives a plain http:// request no TLS phase"""
    assert close(phases(PLAIN_HTTP), {"dns": 18.1, "connect": 239.5, "tls": 0.0, "wait": 257.5})


def test_reused_connection():
    """gives a reused connection only a wait"""
    assert close(phases(REUSED), {"dns": 0.0, "connect": 0.0, "tls": 0.0, "wait": 11.8})


def test_dominant():
    """names the longest phase"""
    assert dominant({"dns": 412.0, "connect": 19.0, "tls": 21.0, "wait": 18.0}) == "dns"
    assert dominant(phases(SLOW_SERVER)) == "wait"
A hint

Split the line on spaces, then each field on =: name, value = field.split("="). Multiply each value by 1,000 to get milliseconds (skip the http field, which is a version, not a time). Each phase is the difference between two moments, such as connect - namelookup. For dominant, max(durations, key=durations.get) returns the key with the largest value.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 Put the steps of a first visit to an HTTPS page over TCP in the order they happen.

    Give each item its position, from 1 (first).

    Show the answer to question 1

    Answer:

    1. The DNS lookup turns the name into an IP address
    2. The TCP handshake opens the connection
    3. The TLS handshake agrees keys and checks the certificate
    4. The browser sends the HTTP request
    5. The first byte of the response arrives

    The browser needs an address before it can send any packet, a connection before TLS can run on it, and keys before it may send the request, because HTTPS encrypts the request too. Only then does the response start.

  2. Question 3 of 5 The round-trip time to a server is 80 ms and the name is already cached. How many milliseconds pass before the browser can send its HTTP request over a new TCP connection with TLS 1.3?

    Type a number.

    Show the answer to question 3

    Answer: 160 ms

    The TCP handshake takes one round trip and the TLS 1.3 handshake one more, so the request leaves after 2 × 80 = 160 ms. QUIC combines the two handshakes and would need 80 ms.

  3. Question 4 of 5 In the lesson's measurements, a second request on the same connection reached its first byte in about 12 ms, against about 52 ms for a first visit. What did it skip?

    Choose one answer.

    Show the answer to question 4

    Answer: The DNS lookup, the TCP handshake and the TLS handshake

    curl printed zeros for namelookup, connect and appconnect, and new=0: it reused the open, encrypted connection, so only the request's own round trip and the server's time were left.

  4. Question 5 of 5 A client may send its request in the first flight with TLS 1.3 or QUIC 0-RTT resumption. Which statements are true?

    Choose every answer that is right.

    Show the answer to question 5

    Answer:

    • An attacker on the path can replay 0-RTT data, so it should only carry requests that are safe to repeat
    • It saves a round trip, because the request does not wait for the handshake to finish

    0-RTT needs a key from an earlier connection, so it only works when the client resumes. The data is encrypted, but TLS cannot stop an attacker from sending the same 0-RTT flight again, which is why a payment or any other request that changes something must wait for the full handshake.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.