How We Measure HTTP Timing: DNS, TCP, TLS, TTFB Breakdown
Source: Dev.to

When you check a website’s response time, “847 ms” doesn’t tell you much. Is it a slow DNS lookup? Network latency? The server itself? You need a breakdown.
At upsonar.io we show exactly where the time goes. Here’s how we do it with Go’s net/http/httptrace package.
The problem
start := time.Now()
resp, _ := http.Get("https://example.com")
fmt.Println(time.Since(start)) // 847ms
847 ms – but why? It could be any part of the request lifecycle.
The solution
httptrace hooks into every phase of an HTTP request. You attach callbacks, and they fire as each phase completes:
trace := &httptrace.ClientTrace{
DNSStart: func(_ httptrace.DNSStartInfo) {
dnsStart = time.Now()
},
DNSDone: func(_ httptrace.DNSDoneInfo) {
fmt.Printf("DNS: %v\n", time.Since(dnsStart))
},
// same pattern for TCP, TLS, etc.
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
What each metric means
- DNS Lookup – Resolving the domain to an IP address. > 100 ms often indicates DNS server issues or a cold cache.
- TCP Connect – The three‑way handshake. Mostly affected by physical distance to the server.
- TLS Handshake – Negotiating encryption. Typical values are 50–150 ms; > 300 ms suggests server or certificate‑chain problems.
- TTFB (Time To First Byte) – Includes DNS, TCP, TLS, and server processing. The standard metric for measuring response speed.
- Transfer – Downloading the response body.
Working example
package main
import (
"crypto/tls"
"fmt"
"io"
"net/http"
"net/http/httptrace"
"os"
"time"
)
func main() {
url := "https://httpbin.org/delay/2"
if len(os.Args) > 1 {
url = os.Args[1]
}
var dnsStart, tcpStart, tlsStart time.Time
totalStart := time.Now()
trace := &httptrace.ClientTrace{
DNSStart: func(_ httptrace.DNSStartInfo) { dnsStart = time.Now() },
DNSDone: func(_ httptrace.DNSDoneInfo) { fmt.Printf("DNS: %v\n", time.Since(dnsStart)) },
ConnectStart: func(_, _ string) { tcpStart = time.Now() },
ConnectDone: func(_, _ string, _ error) { fmt.Printf("TCP: %v\n", time.Since(tcpStart)) },
TLSHandshakeStart: func() { tlsStart = time.Now() },
TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { fmt.Printf("TLS: %v\n", time.Since(tlsStart)) },
GotFirstResponseByte: func() { fmt.Printf("TTFB: %v\n", time.Since(totalStart)) },
}
req, _ := http.NewRequest("GET", url, nil)
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
client := &http.Client{Transport: &http.Transport{DisableKeepAlives: true}}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
transferStart := time.Now()
io.ReadAll(resp.Body)
fmt.Printf("Transfer: %v\n", time.Since(transferStart))
fmt.Printf("Total: %v\n", time.Since(totalStart))
}
Run the program:
go run main.go https://httpbin.org/delay/2
Sample output
DNS: 148.00525ms
TCP: 142.56925ms
TLS: 289.685ms
TTFB: 2.956339583s
Transfer: 72.208µs
Total: 2.95670575s
TTFB is everything up to the first byte. Transfer is the time to download the body – here it’s tiny, so the total time ≈ TTFB.
Larger response example
go run main.go https://proof.ovh.net/files/10Mb.dat
Sample output
DNS: 3.621333ms
TCP: 54.364208ms
TLS: 116.879041ms
TTFB: 286.073291ms
Transfer: 14.221007833s
Total: 14.507351083s
With a 10 MB file, Transfer dominates the overall latency.
Watch out for connection reuse
Go’s http.Client reuses connections by default. Subsequent requests to the same host will show 0 ms for DNS, TCP, and TLS because the connection is already established.
For accurate measurements, disable keep‑alives:
client := &http.Client{
Transport: &http.Transport{
DisableKeepAlives: true,
},
}
What slow numbers tell you
- DNS > 100 ms → Try a faster DNS provider (e.g., Cloudflare, Google).
- TCP > 100 ms → Server is far from users; consider a CDN.
- TLS > 200 ms → Check the certificate chain; enable session resumption.
- TTFB > 500 ms → Backend bottleneck: slow DB, cold starts, heavy processing.
- Transfer high → Large payload; enable gzip/Brotli compression.
The 200 ms rule
Users notice delays over ~200 ms. If your TTFB alone exceeds that threshold, the page will feel slow regardless of frontend optimizations.
Try it on your own site with the free, no‑signup tool: upsonar.io/tools/diagnose.