Learn how to capture and read CPU, memory, goroutine, block, and mutex profiles with net/http/pprof and go tool pprof.
· 6 min read

How to profile Go programs with pprof


Go includes the pieces you need to capture and inspect runtime profiles. The net/http/pprof package exposes profiling data over HTTP, while go tool pprof lets you analyze it from the command line or a web UI. Together they can show where a program spends CPU time, what it allocates, where goroutines are waiting, and which locks are contended.

This post covers the full workflow: wiring up the profiler, capturing profiles, and reading the output. You’ll see how to run a CPU profile, interpret a memory profile, and use the less common profile types when concurrency is the problem.

Setting up net/http/pprof

The net/http/pprof package registers HTTP handlers on DefaultServeMux as a side effect of being imported. If your application already uses the default mux, a blank import is all you need:

package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "hello")
	})

	// pprof endpoints are now available at /debug/pprof/
	http.ListenAndServe(":8080", nil)
}

If you use a custom mux, register the handlers on that mux instead. The package exports Index, Cmdline, Profile, Symbol, and Trace, plus Handler(name) for profiles such as heap, goroutine, block, and mutex. Its init function only installs handlers on http.DefaultServeMux.

Don’t expose pprof publicly in production. Profile data reveals function names, goroutine stacks, command-line arguments, and other internals. Put the endpoints on a private listener or protect them with appropriate network and authentication controls.

If you’re building a REST API and want to add profiling alongside your application routes, Build a REST API in Go with net/http covers the general pattern.

Capturing a CPU profile

CPU profiling samples the code running on the CPU. Start the server, put it under a representative workload, then point go tool pprof at it:

go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30

This tells the server to enable CPU profiling for 30 seconds, then downloads the result. The Profile handler in net/http/pprof calls runtime/pprof.StartCPUProfile, waits for the requested duration, and then calls StopCPUProfile.

Once it finishes, you land in an interactive shell.

Reading pprof output: top and list

Two commands do most of the work: top and list.

top

top shows the functions consuming the most CPU (or memory, depending on the profile type):

(pprof) top 10
Showing nodes accounting for 4.2s, 85% of 4.9s total
      flat  flat%   sum%        cum   cum%
     1.8s 36.73% 36.73%      1.8s 36.73%  runtime.memmove
     0.6s 12.24% 48.98%      0.6s 12.24%  encoding/json.stateInString
     ...

flat is the sampled time attributed directly to a function. cum (cumulative) also includes time attributed to functions it calls. A function with low flat but high cum is often delegating expensive work; high flat means the work is happening in the function itself.

list

Once you spot a suspicious function, drill into its source:

(pprof) list stateInString

This prints the source with samples attributed to individual lines, which is often the quickest way to find the hot path.

web

If you have Graphviz installed, web opens a call graph in your browser. Thicker edges mean more time. Useful for understanding call chains, but top and list are usually faster for pinpointing the actual problem.

Heap and allocation profiling

The two memory profiles you’ll use most often are:

  • /debug/pprof/heap — a sample of allocations for live heap objects
  • /debug/pprof/allocs — a sample of all past allocations
# Current heap usage
go tool pprof http://localhost:8080/debug/pprof/heap

# Total allocations
go tool pprof http://localhost:8080/debug/pprof/allocs

Inside the pprof shell, you can switch between inuse_space, inuse_objects, alloc_space, and alloc_objects:

(pprof) sample_index=inuse_space
(pprof) top

inuse_space estimates bytes allocated and not yet freed, which helps uncover unexpected retention. alloc_space estimates the total bytes allocated over the program’s lifetime, making allocation-heavy paths easier to spot.

A program can have a modest live heap while still allocating heavily. In that case, reducing unnecessary allocations in hot paths can lower garbage-collector CPU work. Preallocating slices or avoiding needless conversions may help; pooling is worth considering only after measurement because it adds complexity of its own.

If you suspect goroutines are the source of allocation pressure, Common Goroutine Leaks in Go and How to Avoid Them covers the patterns that lead to leaked goroutines accumulating memory.

Goroutine profiling

go tool pprof http://localhost:8080/debug/pprof/goroutine

This captures a snapshot of all goroutine stacks. top aggregates stack samples by function, so a large count at the same stack location can point to a backlog or a leak. Confirm it by inspecting the relevant stacks and comparing snapshots over time; a high goroutine count by itself is not proof of a leak.

You can also view goroutine stacks as text. /debug/pprof/goroutine?debug=1 uses the legacy profile format and groups identical stacks with counts. /debug/pprof/goroutine?debug=2 produces a full goroutine dump in the same format as an unrecovered panic, including goroutine IDs and creation stacks where available.

Block and mutex profiling

Two less commonly used profile types that matter when you have concurrency problems.

Block profiling records stack traces that lead to blocking on synchronization primitives, including channel operations and locks. You need to enable it explicitly:

runtime.SetBlockProfileRate(1) // sample every blocking event

Then fetch it:

go tool pprof http://localhost:8080/debug/pprof/block

Mutex profiling records contention on sync.Mutex and sync.RWMutex:

runtime.SetMutexProfileFraction(1)
go tool pprof http://localhost:8080/debug/pprof/mutex

Both add overhead, especially when configured to sample every event, so use a lower sampling rate or enable them only while investigating. In a block profile, cumulative values help identify call paths where goroutines spent the most time waiting. A mutex profile attributes contention to the stacks of goroutines that released contended locks.

Profiling without an HTTP server

If your program isn’t a server (a CLI tool, a batch job), you can write profiles directly using runtime/pprof:

package main

import (
	"os"
	"runtime/pprof"
)

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

	if err := pprof.StartCPUProfile(f); err != nil {
		panic(err)
	}
	defer pprof.StopCPUProfile()

	// ... your program logic here ...
}

Then analyze offline:

go tool pprof cpu.prof

You can also capture heap profiles at specific points by calling pprof.WriteHeapProfile(f). This is helpful for comparing memory state before and after a specific operation.

For programs that use defer heavily in cleanup paths, How to defer in Golang like a pro covers patterns that interact well with profile teardown.

Practical tips

Profile under realistic load. A lightly used service may exercise different code paths from a busy one. Use representative production traffic or a load generator such as hey or vegeta.

Compare before and after. Capture profiles under the same workload, then use a differential profile to see what changed:

curl -o before.prof 'http://localhost:8080/debug/pprof/profile?seconds=30'
# Deploy the change and repeat the same workload.
curl -o after.prof 'http://localhost:8080/debug/pprof/profile?seconds=30'
go tool pprof -diff_base=before.prof after.prof

Don’t optimize without data. Profile first, change one thing, and measure again. That gives you evidence that the change improved the workload you care about.

Use -http for a web UI. Running go tool pprof -http=:9090 cpu.prof serves an interactive interface with flame graphs, source views, and top tables. It can be easier to navigate than the CLI for complex profiles.

The profiles worth reaching for first

CPU and heap profiles are sensible starting points for many performance investigations. Reach for goroutine, block, and mutex profiles when the evidence points to concurrency. net/http/pprof is convenient for a running server, runtime/pprof can write profiles directly, and go tool pprof analyzes the result.

The important part is to profile the workload that matters. A clean profile from an idle service says very little about how it behaves under real traffic.