httpstat: Visualizing HTTP Request Latency with Go's httptrace Package
Hook
Most developers debug slow HTTP requests by adding print statements or checking server logs, never realizing that 80% of their latency might be happening before the server even receives the request.
Context
When an API call takes three seconds to complete, where did that time actually go? Was it DNS resolution talking to a distant nameserver? TCP handshake latency across continents? TLS negotiation with certificate chain validation? Or was the server genuinely slow to respond? Traditional tools make answering this question surprisingly difficult. curl provides timing with --write-out, but requires memorizing cryptic format strings and mental math to calculate phase durations. Browser DevTools show beautiful waterfalls, but only for requests made through the browser. tcpdump and Wireshark provide packet-level truth, but demand deep networking knowledge and post-processing effort.
Dave Cheney's httpstat emerged from this gap: developers needed a tool that could instantly visualize the complete request lifecycle for any HTTP endpoint, with zero configuration and obvious output. Inspired by a Python script of the same name, the Go implementation delivers the same insight but as a single compiled binary with no runtime dependencies. It's curl's timing capabilities reimagined for human comprehension, presenting DNS lookup, TCP connection, TLS handshake, server wait, and content transfer as a colorized timeline that immediately reveals the bottleneck.
Technical Insight
httpstat's elegance lies in its use of Go's httptrace package, which provides hooks into the internal lifecycle events of HTTP requests. Rather than parsing packets or instrumenting network layers, it registers callbacks that the standard library invokes at precise moments during request processing.
The core architecture revolves around httptrace.ClientTrace, a struct containing function fields that get called at key lifecycle events. Here's the fundamental pattern:
var dnsStart, dnsDone, connectStart, connectDone time.Time
trace := &httptrace.ClientTrace{
DNSStart: func(_ httptrace.DNSStartInfo) {
dnsStart = time.Now()
},
DNSDone: func(_ httptrace.DNSDoneInfo) {
dnsDone = time.Now()
},
ConnectStart: func(_, _ string) {
connectStart = time.Now()
},
ConnectDone: func(_, _ string, _ error) {
connectDone = time.Now()
},
TLSHandshakeStart: func() {
tlsStart = time.Now()
},
TLSHandshakeDone: func(_ tls.ConnectionState, _ error) {
tlsDone = time.Now()
},
GotFirstResponseByte: func() {
firstByte = time.Now()
},
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
resp, err := client.Do(req)
This instrumentation pattern is remarkably powerful because it operates at the abstraction layer where Go's http.Client actually works. You're not guessing from external observation—you're receiving notifications from inside the request execution itself. When DNS resolution completes, your callback fires. When the TLS handshake finishes, you know exactly when.
The tool then performs simple time arithmetic to calculate phase durations: DNS took dnsDone.Sub(dnsStart), TCP connection took connectDone.Sub(connectStart), and so on. What makes the output compelling is the visualization—httpstat prints these durations as a colorized timeline showing both absolute times and relative proportions:
DNS Lookup TCP Connection TLS Handshake Server Processing Content Transfer
[ 23ms | 45ms | 156ms | 892ms | 124ms ]
| | | | |
23ms 68ms 224ms 1116ms 1240ms
This immediately shows that TLS and server processing dominate the request time, while DNS and TCP are relatively minor contributors. The color coding (typically cyan for DNS, green for TCP, yellow for TLS, magenta for server wait, and blue for transfer) provides instant visual parsing.
One subtle architectural decision worth noting: httpstat performs actual HTTP requests using the standard http.Client rather than reimplementing HTTP. This means it automatically inherits connection reuse behavior, redirect following, compression handling, and all other http.Client semantics. The tool isn't a mock or simulator—it's a production HTTP client with enhanced observability. This design choice limits what the tool can measure (you can't instrument connection reuse on subsequent requests because the tool makes only one request), but ensures the measurements reflect real-world behavior.
The project structure is deliberately minimal—essentially a single main.go file with straightforward imperative code. There's no plugin system, no configuration file format to parse, no abstraction layers. This "finished software" philosophy means the codebase is highly readable and modifiable. If you need custom behavior, forking and modifying is genuinely straightforward because there's no framework to learn or architectural layers to navigate.
Gotcha
The most important limitation is philosophical: httpstat is explicitly closed to new features. Dave Cheney has made clear the project is "done" except for bug fixes. This isn't abandonware—it's intentionally finished software. If you need HTTP/2 server push analysis, request comparison, JSON output for monitoring systems, or any other enhancement, you'll need to fork or use a different tool. Several community members have created forks with additional features, but they won't be merged upstream.
Technically, httpstat measures single requests only. If you're debugging connection reuse issues, keep-alive behavior, or performance differences across multiple requests, httpstat won't help because it exits after one request completes. It also can't distinguish between multiple redirects' timing—if your request follows three redirects, you'll see aggregate timing, not per-redirect breakdown. The tool works at the http.Client abstraction level, which means some lower-level details (like TCP retransmissions or packet loss) remain invisible. For those scenarios, you genuinely need packet capture tools like tcpdump.
Output is hardcoded for terminal display. While the timing information is valuable, there's no --json flag for programmatic consumption or integration with monitoring dashboards. The colorization assumes a terminal with ANSI color support, which works fine on modern systems but can produce garbage characters in contexts that don't support color codes. You can redirect output to files, but you'll get the color escape sequences embedded in the text unless you pipe through a tool that strips them.
Verdict
Use if: You're debugging API latency and need to quickly identify whether slowness originates from DNS, network, TLS, or server processing. It's perfect for one-off diagnostics, reproducing user-reported slowness, validating CDN or load balancer configurations, and understanding the impact of geographic distance on request performance. The zero-configuration, single-binary nature makes it ideal for SSH sessions into production servers or inclusion in debugging toolkits. If you value stable, finished software that does one thing reliably, httpstat exemplifies that philosophy. Skip if: You need ongoing monitoring (use Prometheus with blackbox exporter), load testing multiple requests (use hey, wrk, or k6), programmatic integration with other tools (curl with --write-out provides parseable timing), or features beyond basic timing visualization. The project's closed-to-features stance means if the current functionality doesn't meet your needs, it never will—evaluate alternatives immediately rather than hoping for enhancements.