When you check a website's response time, "847ms" doesn't tell you much. Is it slow DNS? 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
847ms. But why? Could be anything.
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 domain to IP. Slow (100ms+) means DNS server issues or cold cache.
TCP Connect - the three-way handshake. Mostly about physical distance to the server.
TLS Handshake - negotiating encryption. Usually 50-150ms. Over 300ms means something's off - slow server or problematic certificate chain.
TTFB - time to first byte. Includes DNS + TCP + TLS + server processing. This is 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))
}
go run main.go https://httpbin.org/delay/2
Output:
DNS: 148.00525ms
TCP: 142.56925ms
TLS: 289.685ms
TTFB: 2.956339583s
Transfer: 72.208µs
Total: 2.95670575s
TTFB is everything until the first byte arrives. Transfer is downloading the body - here it's tiny. Total = TTFB + Transfer.
With a larger response, Transfer becomes significant:
go run main.go https://proof.ovh.net/files/10Mb.dat
DNS: 3.621333ms
TCP: 54.364208ms
TLS: 116.879041ms
TTFB: 286.073291ms
Transfer: 14.221007833s
Total: 14.507351083s
Same TTFB formula, but now Transfer dominates - 14 seconds to download 10MB.
Watch out for connection reuse
Go's http.Client reuses connections by default. Second request to the same host - DNS, TCP, TLS all show 0ms.
For accurate measurements, disable keep-alives:
client := &http.Client{
Transport: &http.Transport{
DisableKeepAlives: true,
},
}
What slow numbers tell you
- DNS > 100ms → Try a faster DNS provider (Cloudflare, Google)
- TCP > 100ms → Server is far from users, consider a CDN
- TLS > 200ms → Check certificate chain, enable session resumption
- TTFB > 500ms → Backend problem: slow database, cold starts, heavy processing
- Transfer high → Large response, enable gzip/brotli compression
The 200ms rule
Users notice delays over 200ms. If your TTFB alone exceeds that, the page will feel slow - no matter how optimized your frontend is.
Try it on your site: upsonar.io/tools/diagnose - free, no signup.
What tools do you use for debugging slow requests?
Top comments (0)