httptrace is one of those packages that ships with Go that more people should know about. It's in net/http/httptrace and it gives you visibility into every phase of an HTTP request — DNS lookup, TCP connection, TLS handshake, and the actual request — without adding any external dependencies.
The setup
You attach a *httptrace.ClientTrace to a request context. Go calls the relevant hook as each phase completes. Here's a minimal example that just prints timestamps:
package main
import (
"context"
"fmt"
"net/http/httptrace"
"net/http"
"crypto/tls"
"time"
)
var start time.Time
func trace() *httptrace.ClientTrace {
return &httptrace.ClientTrace{
DNSStart: func(info httptrace.DNSStartInfo) {
fmt.Printf("DNS lookup started: %s\n", info.Host)
},
DNSDone: func(info httptrace.DNSDoneInfo) {
fmt.Printf("DNS resolved: %v (duration: %s)\n", info.Addrs, time.Since(start))
},
ConnectStart: func(network, addr string) {
fmt.Printf("Connecting to %s...\n", addr)
},
ConnectDone: func(network, addr string, err error) {
if err != nil {
fmt.Printf("Connection error: %v\n", err)
} else {
fmt.Printf("Connected to %s\n", addr)
}
},
TLSHandshakeStart: func() {
fmt.Printf("TLS handshake starting\n")
},
TLSHandshakeDone: func(state tls.ConnectionState, err error) {
fmt.Printf("TLS handshake done, version: %x\n", state.Version)
},
WroteRequest: func(reqInfo httptrace.WroteRequestInfo) {
if reqInfo.Err != nil {
fmt.Printf("Request write error: %v\n", reqInfo.Err)
}
},
GotConn: func(info httptrace.GotConnInfo) {
if info.Reused {
fmt.Printf("Connection reused (idle: %s)\n", time.Since(info.LastUsed))
} else {
fmt.Printf("New connection established\n")
}
},
}
}
func main() {
start = time.Now()
req, _ := http.NewRequest("GET", "https://example.com", nil)
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace()))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Request failed: %v\n", err)
return
}
defer resp.Body.Close()
fmt.Printf("Response status: %s (total time: %s)\n", resp.Status, time.Since(start))
}
Where this actually helps
The most common use is diagnosing unexpected latency in an HTTP client. If your service calls an upstream API and responses are slower than expected, httptrace tells you whether the delay is in DNS, the TCP handshake, TLS negotiation, or something else.
A pattern I use: wrap httptrace in a small helper that collects timings into a struct and logs them if a request exceeds a threshold. Something like:
type requestTimings struct {
DNS time.Duration
Connect time.Duration
TLS time.Duration
Total time.Duration
}
The hooks give you time.Time values for each event, so arithmetic is straightforward.
Connection reuse tracking
One underappreciated feature: GotConn fires when a connection is either reused or freshly created. You can tell whether your client is keeping connections alive or spinning up new ones for every request — which matters a lot for high-volume clients hitting the same host repeatedly.
One thing to watch
httptrace hooks fire synchronously on the goroutine managing the connection. Keep them fast — don't do I/O or acquire locks in a hook, or you'll distort your own timings.
That's it. No external packages, no magic. If you're debugging an HTTP client and want to know where time is going, httptrace is worth knowing about.
Top comments (0)