Open this lesson in your favourite AI. It'll walk you through the why, explain the demo, and quiz you on the try-it list.
A single HTTP request is not one event — it is a pipeline of up to seven distinct phases: DNS resolution, TCP connection, TLS handshake, request send, server think-time, response header receipt (TTFB), and body transfer. Slowness can hide in any one of these phases, and the fix for each is completely different. A request that takes 800 ms because of DNS misconfiguration needs a DNS fix, not a database index. A request slowed by TLS renegotiation needs session resumption, not a cache layer. Without knowing which phase is slow you are guessing — and the wrong guess wastes a sprint.
Percentile statistics expose the shape of a latency distribution that averages permanently hide. A workload where 990 requests take 10 ms and 10 take 5,000 ms produces a 59 ms average — a number that reads as healthy while 1% of users wait five seconds. Collecting a sample of 200 requests and comparing p50, p95, and p99 makes that tail visible as a concrete number rather than an invisible outlier.
https://httpbin.org/delay/0.5 — the time_starttransfer field is your TTFB. Subtract time_appconnect from it to isolate pure server think-time.Use these three in order. Each builds on the one before.
In one paragraph, explain the seven phases of an HTTP request and what each one represents.
Explain what happens during a TLS handshake and why it can add 100–300 ms to the first request but near-zero on subsequent ones to the same server.
I see 180 ms of DNS lookup time on every request to my internal microservice, even in production. What are the three most likely causes and how would I confirm each?
// main.go — print per-phase timings by tracing the HTTP connection
package main
import (
"crypto/tls"
"fmt"
"io"
"net/http"
"net/http/httptrace"
"time"
)
func main() {
url := "https://jsonplaceholder.typicode.com/posts/1"
var (
t0 = time.Now()
dnsStart, dnsEnd, connStart, connEnd time.Time
tlsStart, tlsEnd, reqWritten, firstByte time.Time
)
trace := &httptrace.ClientTrace{
DNSStart: func(_ httptrace.DNSStartInfo) { dnsStart = time.Now() },
DNSDone: func(_ httptrace.DNSDoneInfo) { dnsEnd = time.Now() },
ConnectStart: func(_, _ string) { connStart = time.Now() },
ConnectDone: func(_, _ string, _ error) { connEnd = time.Now() },
TLSHandshakeStart: func() { tlsStart = time.Now() },
TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { tlsEnd = time.Now() },
WroteRequest: func(_ httptrace.WroteRequestInfo) { reqWritten = time.Now() },
GotFirstResponseByte: func() { firstByte = time.Now() },
}
req, _ := http.NewRequest("GET", url, nil)
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
resp, _ := http.DefaultClient.Do(req)
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
total := time.Since(t0)
ms := func(a, b time.Time) int64 { return b.Sub(a).Milliseconds() }
fmt.Printf("DNS lookup: %3d ms\n", ms(dnsStart, dnsEnd))
fmt.Printf("TCP connect: %3d ms\n", ms(connStart, connEnd))
fmt.Printf("TLS handshake: %3d ms\n", ms(tlsStart, tlsEnd))
fmt.Printf("Request send: %3d ms\n", ms(tlsEnd, reqWritten))
fmt.Printf("Server (TTFB): %3d ms\n", ms(reqWritten, firstByte))
fmt.Printf("Transfer: %3d ms\n", ms(firstByte, t0.Add(total)))
fmt.Printf("Total: %3d ms\n", total.Milliseconds())
}go run main.go