Go Execution Tracer: Pinpointing Concurrency Bottlenecks
Go's Execution Tracer creates a visual timeline of your program, capturing goroutine state changes, syscalls, and GC events. It's essential for diagnosing subtle concurrency issues like lock contention. The main footgun is misusing annotations for work.
WHY IT EXISTS Standard profilers show you where CPU time is spent, but they don't always explain why. The Go Execution Tracer was created to diagnose complex, time-based interactions in concurrent programs, answering questions like "What was this goroutine waiting for?" or "Why did my whole application pause?".
THE MENTAL MODEL Think of the tracer as a flight data recorder for your Go application. It records thousands of discrete runtime events—a goroutine starting, a syscall blocking, a channel send—with high-precision timestamps. The go tool trace then reconstructs this data into a visual timeline, letting you replay your program's execution and see exactly where time was spent or where contention occurred.
HOW IT WORKS The tracer hooks into the Go runtime to capture a stream of events. You can enable it in several ways: for tests, use the go test -trace=trace.out flag; for a running service, import _ "net/http/pprof" to expose a /debug/pprof/trace endpoint; for standalone apps, manually call trace.Start(writer) and trace.Stop(). The output is a binary trace file that captures goroutine creation/blocking, syscalls, GC events, and heap size changes.
WHEN TO USE IT Use the tracer when you suspect a concurrency-related performance issue. It's ideal for diagnosing high lock contention, poor CPU utilization from blocked goroutines, unexpected garbage collection pauses, or inefficient scheduling. The visualizer helps you spot patterns that are nearly impossible to find in logs or metrics alone.
WHEN NOT TO USE IT Avoid running the tracer continuously in production. It has a non-trivial performance overhead and is meant for temporary, deep-dive diagnostics, not for general monitoring. For high-level performance metrics, use tools like Prometheus. For CPU-bound issues, a standard CPU profile is often a better first step.
ONE CANONICAL EXAMPLE User annotations add business context to a trace. A Region tracks a time interval within a single goroutine, like trace.WithRegion(ctx, "processOrder", ...). A Task tracks a logical operation that may span multiple goroutines. You might create a Task for an API request to see how it fans out to various workers, giving a complete end-to-end view of that request's lifecycle, which a Region cannot provide.
Read the original → pkg.go.dev
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.