Introduction
Beyond profiling, Go provides runtime metrics for continuous monitoring. These let you track GC behavior, goroutine counts, and memory usage in real time — essential for detecting performance regressions before they become outages.
Key Concepts
- runtime/metrics: The standard package for reading runtime statistics (GC pauses, heap size, goroutine count).
- GODEBUG: An environment variable that enables runtime debugging output (GC logging, scheduler tracing).
- expvar: A package that exposes application metrics as JSON via HTTP.
- Goroutine leak profile (Go 1.26, experimental): A new pprof profile type that detects goroutines blocked on unreachable concurrency primitives. Requires
GOEXPERIMENT=goroutineleakprofile.
Real World Context
In production, you cannot attach a profiler every time something goes wrong. Instead, you export runtime metrics to Prometheus, Datadog, or similar systems. When the heap size doubles or goroutine count climbs steadily, you get an alert before the OOM killer strikes.
Deep Dive
runtime/metrics Package
The runtime/metrics package provides structured access to runtime statistics:
goimport "runtime/metrics" // Read specific metrics samples := []metrics.Sample{ {Name: "/gc/pauses:seconds"}, {Name: "/memory/classes/heap/objects:bytes"}, {Name: "/sched/goroutines:goroutines"}, } metrics.Read(samples)
This is more reliable than runtime.ReadMemStats() because it does not stop-the-world.
GODEBUG for Diagnostic Output
The GODEBUG environment variable enables diagnostic logging:
bash# Log every GC cycle with pause times GODEBUG=gctrace=1 ./myapp # Log scheduler events (very verbose) GODEBUG=schedtrace=1000 ./myapp
The gctrace=1 output shows heap size, GC pause duration, and CPU utilization per cycle — invaluable for tuning GOGC.
Goroutine Leak Detection (Go 1.26, Experimental)
Go 1.26 introduces an experimental goroutine leak profile (requires GOEXPERIMENT=goroutineleakprofile) that detects goroutines blocked on unreachable concurrency primitives:
bash# Build with: GOEXPERIMENT=goroutineleakprofile go build go tool pprof http://localhost:6060/debug/pprof/goroutineleak
It detects goroutines blocked on channels, mutexes, or sync.Cond where the primitive itself is unreachable — a strong signal of a leak. This feature is expected to be enabled by default in Go 1.27.
expvar for Application Metrics
goimport "expvar" var requestCount = expvar.NewInt("requests_total") func handler(w http.ResponseWriter, r *http.Request) { requestCount.Add(1) // ... } // Metrics available at /debug/vars
Common Pitfalls
- Relying on runtime.NumGoroutine() alone for leak detection — The number fluctuates normally. Use the goroutine profile or Go 1.26's leak profile for accurate detection.
- Using runtime.ReadMemStats() in hot paths — It stops the world briefly. Use
runtime/metricsinstead for non-blocking reads.
Best Practices
- Export metrics to your monitoring system — Use
runtime/metricsto feed Prometheus or Datadog. Set alerts on heap growth rate and goroutine count. - Enable gctrace in staging — Run with
GODEBUG=gctrace=1during load tests to understand GC behavior under realistic load.
Summary
runtime/metricsprovides non-blocking access to GC, heap, and goroutine statistics.GODEBUG=gctrace=1logs GC cycle details;schedtrace=1000logs scheduler state.- Go 1.26 adds an experimental goroutine leak profile (requires
GOEXPERIMENT=goroutineleakprofile) for detecting goroutines blocked on unreachable primitives. expvarprovides simple HTTP-accessible application metrics.- Always monitor runtime metrics in production to catch performance regressions early.
Code Examples
package main
import (
"fmt"
"runtime/metrics"
)
func printRuntimeMetrics() {
// Define which metrics to read
samples := []metrics.Sample{
{Name: "/gc/pauses:seconds"},
{Name: "/memory/classes/heap/objects:bytes"},
{Name: "/sched/goroutines:goroutines"},
}
// Read without stopping the world
metrics.Read(samples)
for _, s := range samples {
fmt.Printf("%s = %v\n", s.Name, s.Value)
}
}