How do you capture a Go heap profile with runtime/pprof, and why call runtime.GC() first?
answer
- one call from runtime/pprof
- it wants an open file
- same profile from go test
- live numbers are one cycle stale
- force a collection before writing
basics
~20 sOpen a file and call runtime/pprof.WriteHeapProfile on it, usually right after runtime.GC(), because the in-use numbers come from the most recently completed collection. In tests, go test -memprofile mem.out writes the same profile for you.
solid answer
~40 sThere are three usual routes. In code, create a file and call `pprof.WriteHeapProfile(f)` from `runtime/pprof`; that is shorthand for `pprof.Lookup("heap").WriteTo(f, 0)`, and `pprof.Lookup("allocs")` gives the same data with a different default view. In tests and benchmarks, `go test -memprofile mem.out` writes it at the end of the run. A long-lived service normally exports the same profile over an HTTP handler instead. The important detail is staleness: the live-memory figures in a heap profile are those of the last completed GC cycle, so call `runtime.GC()` immediately before writing if you want a current snapshot. Then read it with `go tool pprof ./binary mem.out`. Remember it is sampled, not a census of every allocation.
code
go · 10 linesf, err := os.Create("heap.out")
if err != nil {
log.Fatal(err)
}
defer f.Close()
runtime.GC() // live figures come from the last completed GC cycle
if err := pprof.WriteHeapProfile(f); err != nil {
log.Fatal(err)
}go deeper
Be ready to name the call and write it from memory: create a file, runtime.GC(), pprof.WriteHeapProfile(f), close the file. Also know that go test -memprofile gives you the same file.
Explain the staleness rule - live figures come from the last finished collection - and that WriteHeapProfile is shorthand for Lookup("heap").WriteTo(w, 0). Know that the profile is sampled, not exhaustive.
Show judgment about when to take it: under representative load, not at shutdown, and with awareness that forcing a collection costs a real pause on a busy process.
Frame it as a capability rather than a one-off: which builds can be profiled, where profiles are stored, and who is expected to be able to pull one during an incident without shipping new code.
## What a Go heap profile is A Go heap profile is a table of **allocation sites**: for each call stack that allocated memory, the runtime keeps four numbers — how many objects that stack has allocated since the process started, how many bytes, how many of those objects are still live, and how many of those bytes are still live. It is produced by the Go runtime itself (no agent, no external tool), it is sampled rather than exhaustive, and it is written in the protobuf `pprof` format that `go tool pprof` reads. It is not a CPU profile. A CPU profile answers "where is time going"; a heap profile answers "where is memory coming from, and what is still holding it". ## Route 1: from code, with runtime/pprof The canonical snippet is four lines: ```go f, err := os.Create("heap.out") if err != nil { log.Fatal(err) } defer f.Close() runtime.GC() if err := pprof.WriteHeapProfile(f); err != nil { log.Fatal(err) } ``` `pprof.WriteHeapProfile(w)` is a convenience wrapper for `pprof.Lookup("heap").WriteTo(w, 0)`. The `0` is the debug level: `0` writes the binary protobuf format that tooling expects; `1` writes a legacy human-readable text form with a `runtime.MemStats` dump appended, which is occasionally handy when you cannot get the file off the machine but can read a log. `pprof.Lookup` also knows the name `"allocs"`. That is the *same* underlying data; the two names differ only in which view `go tool pprof` opens by default. ## Route 2: from tests and benchmarks `go test -memprofile mem.out ./...` writes a memory profile for the package under test, and `go test -bench=. -benchmem -memprofile mem.out` does it for a benchmark while also printing bytes-per-operation and allocations-per-operation in the benchmark output. `testing.B.ReportAllocs()` forces those per-op columns on for one benchmark without the flag. For benchmarks this is usually the fastest loop: change code, re-run, compare allocs/op, and open the profile when the number moves the wrong way. `go test` also accepts `-memprofilerate` to change the sampling rate for that run. ## Route 3: from a running service Production services normally export the profile over an HTTP endpoint so you can pull it without restarting anything. The mechanics of that endpoint are a separate subject; what matters here is that the artefact is identical — the same sampled table, read with the same tool. ## Why runtime.GC() before writing The *live* columns of a heap profile (in-use objects and in-use bytes) are computed from the most recently finished garbage-collection cycle. If your program has been allocating heavily since that cycle ended, the profile understates what is live, and objects that have already become garbage but have not been collected yet still count as live. Calling `runtime.GC()` immediately before writing forces a collection so the snapshot reflects the state right now. The cumulative columns (everything ever allocated) do not need this, but a forced GC does not hurt them. The cost is a full collection, which is a real stop-the-world-adjacent pause on a busy service, so it is a deliberate choice rather than something you sprinkle everywhere. ## Reading it `go tool pprof ./binary heap.out` opens an interactive session; the binary is passed so the tool can symbolise addresses. Everything you then do — top, listing a function, opening a flame graph in a browser — works on the four columns above. ## Things people get wrong * Assuming it records every allocation. It records a statistical sample; the numbers are scaled estimates. * Writing the profile without closing or flushing the file, then wondering why the tool reports a truncated profile. * Reaching for a heap profile to explain high CPU, or a CPU profile to explain memory growth. * Taking the profile at process exit and being surprised that almost nothing is live — by then most work is done and much has been collected.
- What changes if you call pprof.Lookup("heap").WriteTo(f, 1) instead of WriteTo(f, 0)?Debug level 0 writes the binary pprof protobuf that `go tool pprof` consumes. Level 1 writes a legacy human-readable text listing of the sampled stacks with a `runtime.MemStats` dump appended, which is readable straight out of a log but is not what the analysis tooling expects.
- How do you get a memory profile out of a benchmark rather than a running service?`go test -bench=. -benchmem -memprofile mem.out` runs the benchmark, prints bytes and allocations per operation, and writes the profile. `testing.B.ReportAllocs()` turns the per-op columns on for a single benchmark without the flag. Then open it with `go tool pprof` against the test binary.
- You wrote the profile at the very end of main and it shows almost nothing live. Why?The live columns describe memory still reachable at the snapshot. At shutdown the work is finished, caches are dropped and most objects have been collected, so in-use numbers collapse. Take the snapshot while the program is under the load you care about, or read the cumulative allocation columns instead.
saying these in an interview costs you the question
- Thinks a heap profile records every single allocation
- Confuses WriteHeapProfile with StartCPUProfile
- Never forces a collection, then trusts stale live numbers
- Forgets to close the file and gets a truncated profile
- Believes the profile needs an external agent or tool to collect