status
this chapter is in active development
expect live edits and rapid iteration (except for when i am really busy with other stuff) while this material is written.
status
this chapter is in active development
expect live edits and rapid iteration (except for when i am really busy with other stuff) while this material is written.
time to prove every phase actually happened.
no vibes, no "works on my machine."
grab packets, syscalls, timestamps, and beat the timeline into submission.
sudo tcpdump -i eth0 -w request.pcap host example.org and \( port 53 or port 443 \)
curl -w '\nnamelookup: %{time_namelookup}s\nconnect: %{time_connect}s\ntls: %{time_appconnect}s\nttfb: %{time_starttransfer}s\ntotal: %{time_total}s\n' -v https://example.org/ > /dev/null
tcpdump writes literally everything into request.pcap.
the curl -w formatter prints a timing breakdown so you can map human time to packets.
tack on --trace-time trace.log if you want per-byte logs; it is noisy and perfect.
here is real output from curl -v https://example.org:
* Host example.org:443 was resolved.
* IPv6: 2600:1408:ec00:36::1736:7f2f, 2600:1406:5e00:6::17ce:bc29, ...
* IPv4: 23.215.0.132, 23.215.0.133, 23.220.75.235, 23.220.75.238
* Trying [2600:1408:ec00:36::1736:7f2f]:443...
dns returned multiple addresses for both ipv4 and ipv6. curl tries ipv6 first (happy eyeballs) and falls back to ipv4 if it fails. if you only see one family here, check your resolver or network config.
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
the tls handshake in sequence. OUT is what curl sent, IN is what arrived. the Change cipher spec messages are compatibility artifacts from tls 1.2 - tls 1.3 sends them but they do not mean anything. if any of the real handshake messages stall or never appear, that is where your problem lives.
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / x25519 / id-ecPublicKey
* ALPN: server accepted h2
* Server certificate:
* subject: C=US; ST=California; L=Los Angeles; O=Internet Corporation for Assigned Names and Numbers; CN=*.example.org
* start date: Jan 15 00:00:00 2025 GMT
* expire date: Jan 15 23:59:59 2026 GMT
* subjectAltName: host "example.org" matched cert's "example.org"
* issuer: C=US; O=DigiCert Inc; CN=DigiCert Global G3 TLS ECC SHA384 2020 CA1
* SSL certificate verify ok.
negotiated cipher (TLS_AES_256_GCM_SHA384), key exchange (x25519), and certificate details. SSL certificate verify ok means the chain validated against curl's trust store. if this says anything else, you have cert problems - expired, wrong hostname, or missing intermediate.
* Connected to example.org (2600:1408:ec00:36::1736:7f2f) port 443
* using HTTP/2
> GET / HTTP/2
> Host: example.org
> User-Agent: curl/8.14.1
> Accept: */*
>
* Request completely sent off
< HTTP/2 200
< content-type: text/html
< cache-control: max-age=86000
< content-length: 513
< alt-svc: h3=":443"; ma=93600
http/2 request and response. > lines are what curl sent, < lines are what came back. alt-svc advertises http/3 support on the same port - a quic-capable client might switch to udp on the next request.
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection #0 to host example.org left intact
after the response, the server sent session tickets for resumption. "left intact" means keep-alive worked and the connection stayed open. if you see "closing connection" instead, something forced a teardown.
dns - the "was resolved" line. time_namelookup covers this. if it is zero, you hit cache.
tcp - hidden inside "Trying [...]:443". time_connect measures syn to ack.
tls - the handshake messages above. time_appconnect ends when both Finished messages land.
http - the > and < lines. time_starttransfer (ttfb) is when the first response byte arrived.
namelookup: 0.012s
connect: 0.034s
tls: 0.071s
ttfb: 0.118s
total: 0.145s
dns took 12 ms because the recursive cache bailed us out.
tcp spent ~22 ms (34-12), which lines up with one rtt to the server.
tls added ~37 ms because tls 1.3 still needs one round trip unless resuming.
ttfb-total gap is the response body, which stayed tiny here.
if your numbers look different, congratulations - you have a story to tell.
slow dns shows up as repeated queries in the capture, rising time_namelookup, zero tcp packets. call your resolver, not your backend team.
firewall drop looks like endless syn retransmits, time_connect climbing, curl timing out. nothing east of the firewall even saw you.
certificate drama presents as tls alerts (fatal, unknown ca etc.) right after Certificate. curl prints SSL certificate problem. renew the cert or ship the missing intermediate.
http 5xx is transport behaving, but the status code is >=500 and tcp closes politely. stop blaming the network; fix the app.
open request.pcap in wireshark and "Follow TLS Stream" to see timing even without decrypting.
tshark -r request.pcap -Y 'tcp.flags.syn==1' -T fields -e frame.time_relative gives you handshake timestamps for scripts or dashboards.
strace -ttt -e connect,sendto,recvfrom curl ... lines syscalls up with packet timestamps so you know exactly where userland blocks.
once you can narrate this trace, the internet stops being a haunted box.
you know which phase ate your budget, which component failed, and whose pager should actually ring.
this chapter covered the anatomy of a single request. you now know how dns, tcp, tls, and http stack up and where each one can fail.
chapter 2 digs into how packets actually find their way across networks - ip addresses, subnets, routing tables, nat, and the arp/ndp machinery that glues it all together at layer 2.