Introduction

Go's pprof package provides CPU, memory, goroutine, and mutex profiling, but improper usage leads to empty profiles, excessive overhead, or misleading results. Starting a CPU profile while one is already running returns cpu profiling already in use. Profiles that are too short do not capture enough samples to be useful, while profiles taken during idle periods show nothing interesting. The HTTP profiling endpoint (net/http/pprof) is the most convenient way to collect profiles from running services, but it is often left exposed in production or not configured correctly.

Symptoms

bash
cpu profiling already in use

Or empty profile:

bash
go tool pprof -top cpu.prof
# Showing nodes accounting for 0 total, 0% of 0 total
# No samples collected

Or:

bash
failed to fetch any profiles

Common Causes

  • Nested profile start: Starting CPU profile while one is already active
  • Profile duration too short: 1 second not enough to capture meaningful samples
  • Program exits before profile stops: Profile not written to file
  • pprof HTTP not registered: Handler not mounted on mux
  • Profile collected during idle time: No work happening during collection window
  • Compiler optimizations remove function: Inlined functions do not appear in profile

Step-by-Step Fix

Step 1: Use pprof HTTP endpoint

```go import ( "log" "net/http" _ "net/http/pprof" // Registers handlers automatically )

func main() { // pprof handlers registered at /debug/pprof/ // Endpoints: /profile, /heap, /goroutine, /mutex, /block

go func() { // Bind to localhost only - never expose to public log.Println(http.ListenAndServe("localhost:6060", nil)) }()

// Your application code... }

# Collect profiles: # CPU profile (30 seconds) go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# Memory profile go tool pprof http://localhost:6060/debug/pprof/heap

# Goroutine profile go tool pprof http://localhost:6060/debug/pprof/goroutine ```

Step 2: Programmatic profiling with proper lifecycle

```go import ( "os" "runtime/pprof" )

func runWithProfile() error { f, err := os.Create("cpu.prof") if err != nil { return err } defer f.Close()

// Start CPU profile if err := pprof.StartCPUProfile(f); err != nil { return fmt.Errorf("start CPU profile: %w", err) } defer pprof.StopCPUProfile()

// Run the code you want to profile runBenchmarks()

// StopCPUProfile called by defer return nil } ```

Step 3: Analyze profile output effectively

```bash # Top functions by CPU time go tool pprof -top cpu.prof

# Interactive web view go tool pprof -http=:8080 cpu.prof

# Focus on specific function go tool pprof -focus=handleRequest cpu.prof

# Ignore noise (runtime, garbage collector) go tool pprof -ignore=runtime cpu.prof

# Compare two profiles go tool pprof -base old.prof new.prof

# Generate SVG go tool pprof -svg cpu.prof > cpu.svg ```

Prevention

  • Use net/http/pprof for convenient runtime profiling of services
  • Always bind pprof to localhost -- never expose to public networks
  • Collect CPU profiles for at least 30 seconds for meaningful data
  • Ensure the workload you want to profile is active during collection
  • Use -ignore=runtime to filter out Go runtime noise
  • Compare profiles before and after optimizations to verify improvements
  • Add pprof endpoint to staging environments, not production
  • Use go test -cpuprofile=cpu.prof -bench=. for benchmark profiling