Traces
Tracing allows you to observe the behavior of your program during execution. It offers invaluable insights into performance bottlenecks and bugs. Go provides the runtime/trace package to collect event data on your goroutines, heap allocation, etc.
Tracing should focus on parts of your program where performance is critical.
Performance impact
Tracing impacts performance more than logging, so use it wisely.
runtime/trace features
This table describes the fundamental parts of the runtime/trace package:
| Feature | Use case |
|---|---|
| Start/Stop | Bound trace duration |
| Task | Define logical work |
| Region | Break work into phases |
| Log | Add context |
| Context | Propagate causality |
| Runtime events | Diagnose scheduling & GC |
Here is how these features work together:
Trace Session
│
├─ Task (HTTP request)
│ ├─ Region: auth
│ │ └─ Log: user_id=42
│ ├─ Region: db query
│ └─ Region: response write
│
└─ Runtime events (GC, scheduler, syscalls)
Distrbuted tracing
Distributed tracing monitors a request as it travels across services in a distributed system. For example, an online shopper’s request might travel through the login, search, shopping cart, and payment services. Tracing provides the following benefits:
- Enhanced observability, which can reveal bottlenecks.
- Root cause analysis to help you pinpoint the exact service or component responsible for an error.
- Performance optimization by showing you where communication between services is slow.
- Debugging complex interactions between microservices.
Key concepts
To effectively trace, you need to understand these key concepts:
- Unique identifier: Assign a unique ID to the initial request. This ID ties together all logs and events related to the request.
- Propagation: Propagate the ID through all services. You can send it in HTTP request headers or messages in queues.
- Spans: Each service creates a span of time in which it captures information about the role it plays in the request. The span might include timestamps, service names, function calls, and errors.
- Collection and analysis: A central tracing system should collect the spans and stitch them together based on the ID. This provides a full view of the request’s lifecycle across all services.
Basic tracing
This example demonstrates tracing with regions, which let you break your tracing into phases of work. It runs concurrent goroutines:
busyWork simulates work to show scheduler and CPU activity:
func busyWork() {
start := time.Now()
for time.Since(start) < 50*time.Millisecond {
// Burn CPU
}
}
worker simulates real work and annotates it with trace regions. WithRegion defines a phase in the program. It marks a chunk of time in the program and gives it a name. A region is a start time, end time, and a label.
func worker(ctx context.Context, wg *sync.WaitGroup, name string, delay time.Duration) {
defer wg.Done()
// Mark a region of work in the trace
trace.WithRegion(ctx, name, func() {
fmt.Println(name, "starting")
// Simulate blocking work
time.Sleep(delay)
// Simulate CPU work
busyWork()
fmt.Println(name, "finished")
})
}
- Create a file where you will write the trace. This file is later consumed by the go trace tool.
Starttakes the trace file and begins the tracing. This enables global tracing and tells the runtime to emit scheduler, garbage collection, syscall, and other user events.- Defer a call to
Stopso tracing ends when the function returns. - Get the context of the current goroutine.
NewTaskdefines on logical operation named “main-task”. It attaches metadata to the goroutine context.- Defer a call to
Endso the task ends when the function returns. - Create a WaitGroup for concurrent goroutines.
- Each goroutine is passed the context, which links them both to the same tracing task.
func main() {
f, err := os.Create("trace.out")
if err != nil {
panic(err)
}
defer f.Close()
if err := trace.Start(f); err != nil {
panic(err)
}
defer trace.Stop()
ctx := context.Background()
// Create a top-level trace region
ctx, task := trace.NewTask(ctx, "main-task")
defer task.End()
var wg sync.WaitGroup
wg.Add(2)
go worker(ctx, &wg, "worker-1", 100*time.Millisecond)
go worker(ctx, &wg, "worker-2", 200*time.Millisecond)
wg.Wait()
fmt.Println("Program complete")
}
To run the file, use go tool trace. This command opens your default browser and lets you view the tracing information:
go tool trace trace.out
2026/01/17 10:12:02 Preparing trace for viewer...
2026/01/17 10:12:02 Splitting trace for viewer...
2026/01/17 10:12:02 Opening browser. Trace viewer is listening on http://127.0.0.1:33341
HTTP servers
Complex HTTP servers should implement tracing as middleware. You can wrap an HTTP handler with a function that adds tracing to your code. First, create a simple handler. This handler writes a message to the writer:
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, Tracing!")
}
Next, define the trace wrapper. This function takes a HandlerFunc and returns a HandlerFunc.
HandlerFuncis an adaptor that lets you return a function that will be called later by the HTTP server—rather than returning data, you return executable behavior. This function captures theinnerargument and returns it for execution at a later time.- Return a function with that satisfies the
ServeHTTPinterface. trace.NewTaskcreates a logical unit of work that groups activity across time and across goroutines. For example, across a single HTTP request or CLI command. It takes a context and a task type, which is the task name. Here, it takes the request context and the request path. When its called, it creates a task ID that is passed to the context, which lets you follow a single request across goroutines.Endmarks the end of the request in the trace.Logtakes a context, optional category, and message. This is a timestamped key-value annotation that is attached to a task or region trace timeline. It adds human readable context to execution.- Returns the
innerhandler and passes the context along with the Request.
func TraceHandler(inner http.HandlerFunc) http.HandlerFunc { // 1
return func(w http.ResponseWriter, r *http.Request) { // 2
ctx, task := trace.NewTask(r.Context(), r.URL.Path) // 3
defer task.End() // 4
trace.Log(ctx, "HTTP Method", r.Method) // 5
trace.Log(ctx, "URL", r.URL.String())
inner(w, r.WithContext(ctx)) // 6
}
}
func main() {
http.HandleFunc("/", TraceHandler(handler))
fmt.Println("Server is listening on :8080")
http.ListenAndServe(":8080", nil)
}