Profiling in Go: what pprof shows and what it does not show by default
Interview questions on profiling climb a ladder: how do you find what is slow — how do you take a profile — how flat differs from cum — how to hunt a memory leak — why the block profile is empty — can you profile in production. A run answers each step: the CPU profile is 100 samples a second; a function that allocated almost 600 MB is absent from the in-use memory profile; the block and mutex profiles are empty under real contention until you turn them on; and the cost of the CPU profile is within noise.
Full technical treatment
TL;DR
The Go profiler samples: it does not count every call but looks at some rate
at what is running right now. That is the interview answer. The rate is an
implementation detail: in runtime/pprof of go1.24.7 the CPU profile takes
100 samples a second, and the number is hard-coded in the source.
Measured: 203 samples over 2.11 s of one goroutine's work. Hence the main rule for reading a
profile: it is a sample, and a function that ran for 5 ms may not appear in it at
all.
Beyond that comes what separates real understanding from having read about
it. The output of go tool pprof has two time columns: flat — time in
the function itself, cum — together with what it calls. A dispatcher
function has flat 0 and cum 1800 ms — 86.96% of the profile. The heap profile
has two different value types: inuse_space — what is held right now, and
alloc_space — what was allocated over all time. A function that allocated
596.83 MB and held nothing is absent from inuse_space: a leak is hunted in one
value type, pressure on the collector in the other.
Two profiles are off by default. The block and mutex profiles under real
contention gave 0 records until they were turned on with
SetBlockProfileRate and SetMutexProfileFraction. An empty profile does not
mean "no blocking". The goroutine profile shows a suspicious accumulation: a
hundred goroutines waiting on a channel nobody writes to sit under one
stack; what proves a leak is that this stack keeps growing from capture to
capture. The CPU profile is usually lightweight enough for a limited capture in
production: on our workload the overhead did not exceed measurement noise, but
that is not a universal number. And /debug/pprof is an internal surface: it
is not exposed to the outside.
- what a goroutine and a call stack are;
- that the garbage collector frees memory that is no longer used — that is the lesson on the garbage collector;
- how to run tests and benchmarks with
go test.
- how to take a profile: tests,
net/http/pprof,runtime/pprof; - flat and cum,
inuse_spaceandalloc_space, the sampling rate; - the block, mutex and goroutine profiles, the cost of profiling.
What is really being asked
The ladder is almost always this one:
- "How do you find what is slow?" — the warm-up; "pprof" is correct, but you need to start with which profile.
- "How do you take a profile?" — three ways, and each has its place.
- "How does flat differ from cum?" — this checks whether the candidate has read a profile themselves.
- "How do you find a memory leak?" — about the heap profile's two value types.
- "Why is the block profile empty?" — the question that trips up even people who have read profiles.
- "Can you profile in production?" — about the cost, and about who can
reach
/debug/pprof.
The ladder has one spine: a profile is a sample of a particular event, and each profile has its own event, its own rate and its own default. Almost every misreading comes from looking at the wrong profile.
Base: a profile is a sample, not a full count
The profiler does not record every call: that would be far too expensive. Almost every profile is a sample (the exception is the goroutine profile, more on it below): at some rate it takes a sample — a snapshot of the stack at the moment of an event — and adds up identical stacks. What counts as an event depends on the profile:
| profile | event | by default |
|---|---|---|
| CPU | 100 times per second of CPU time (go1.24.7) | turned on by a call |
| heap / allocs | on average one allocation per 512 KiB | on |
| goroutine | every goroutine at the moment of capture | on |
| block | waiting on synchronisation primitives | off |
| mutex | contention on a mutex: the holder's stack, at Unlock | off |
A profile is taken in three ways, and each has its place:
- from a test or benchmark —
go test -bench . -cpuprofile cpu.out, and next to it-memprofile,-blockprofile,-mutexprofile. This is the place for a reproducible load; - from a running service —
import _ "net/http/pprof", and every profile is served under/debug/pprof/. This is the place for real load; - from code —
runtime/pprof:StartCPUProfile,Lookup("heap").WriteTo. Both ways above are built on it; you write it by hand when you need a profile of a specific section.
A profile is read the same way in all cases: go tool pprof — with the
-top, -list and -web modes.
And right away — which tool answers which question:
| tool | the question it answers |
|---|---|
| CPU profile | where the processor spends its time |
| heap / allocs | what is held in the Go heap and who allocates |
| block | where goroutines wait on synchronisation primitives |
| mutex | where mutexes are contended and who holds them |
| goroutine | which goroutines exist now and what they are blocked on |
trace (go tool trace) | what happened over time: scheduler, network, latency |
A Junior-level answer to "how do you find what is slow": take a CPU profile —
from a benchmark with go test -cpuprofile or from a service with
net/http/pprof — and open it in go tool pprof; at the top will be the
functions where the program spends the most time.
Mechanism 1: the CPU profile — a sample, in go1.24.7 100 times a second
In runtime/pprof you do not choose the rate: StartCPUProfile does not take
it, it is hard-coded. In the source the explanation sits right next to it:
100 Hz is a reasonable choice: it is frequent enough to produce useful data,
rare enough not to bog down the system, and a nice round number to make it easy
to convert sample counts to seconds.
So at the nominal 100 Hz one sample corresponds to roughly 10 ms of CPU time — in aggregate, statistically, not as a stopwatch on each function. Two seconds of one goroutine's work:
duration of work 2.11 s
samples in the profile 203
samples per second 96
Two things follow from this number that are worth being able to say.
The profile may not see short functions. A function that ran for 5 ms over the whole run gets half a sample on average — that is, zero or one. A profile answers "where does the program spend a lot of time", not "was the function called".
"Total samples" in the output is time, not a count. In another run —
section 1 of internals.go, a different workload of the same length —
go tool pprof -top prints:
Duration: 2.12s, Total samples = 2070ms (97.61%)
2070 ms is 207 samples converted at the nominal 10 ms. The percentage is CPU time as a share of
wall time: 97.61%, because one goroutine uses at most one core. With several
busy goroutines the percentage can exceed 100 — the run of
bench/goprof/cores.go:
one busy goroutine: Duration: 2.11s, Total samples = 1.97s (93.39%)
two busy goroutines: Duration: 2.15s, Total samples = 3.89s (181.35%)
On two cores, two goroutines accumulated almost twice as much CPU time as passed on the clock.
Mechanism 2: flat and cum
Here is the table you open a profile for. The handle function does nothing
itself — it only calls hashAll (heavy work) and parse (light work). The
rows are picked from the full -top listing, so do not read the sum% column
here — it is the running flat% in the original order:
2) FLAT VERSUS CUM ON THE SAME CALL CHAIN
─────────────────────────────────────────
flat flat% sum% cum cum%
0 0% 94.20% 1750ms 84.54% main.hashAll
50ms 2.42% 77.29% 50ms 2.42% main.parse
0 0% 94.20% 1800ms 86.96% main.handle
- flat — samples in which the function was at the top of the stack: time in its own code.
- cum — samples in which the function was anywhere in the stack: itself plus everything it called.
handle has flat 0 and cum 86.96%: it spends nothing itself, but almost all of
the program's time is spent under it. Sorting by flat would not show it;
sorting by cum would put it at the top.
Note hashAll too: its flat is 0 as well. The heavy work sits deeper — in
crypto/sha256 functions, and those are what stand at the top of -top:
1310ms 63.29% 63.29% 1310ms 63.29% crypto/internal/fips140/sha256.blockAVX2
Hence the reading order worth naming in an interview:
- flat — where the processor was executing directly;
- cum — which call paths are responsible for that time.
You need both. Flat shows the hot code — here blockAVX2; cum shows whose
decision it is to call it: handle via hashAll. Often the win is not in
speeding up the top of flat but in calling less often the path that cum
reveals.
Mechanism 3: the heap profile — two different value types
Every heap profile record carries four value types — allocated and in use, in
objects and in bytes — and -sample_index picks which one to show. The two
main ones answer different questions. The documentation defines the moment the
profile reflects:
The heap profile reports statistics as of the most recently completed garbage
collection; …
Two functions: churn allocates a lot and holds nothing, retain allocates a
little and holds all of it:
3) HEAP PROFILE: alloc_space VERSUS inuse_space
───────────────────────────────────────────────
alloc_space — how much was allocated over all time:
596.83MB 76.83% 76.83% 596.83MB 76.83% main.churn
16.19MB 2.08% 99.58% 16.19MB 2.08% main.retain
inuse_space — how much is held right now:
there is no main.churn line in the output
16.19MB 97.00% 97.00% 16.19MB 97.00% main.retain
The function that allocated almost 600 MB is entirely absent from
inuse_space. That is not a profile error but the point of the two value
types:
inuse_space(the default forheap) — how many bytes of the Go heap are held now. This is where a Go heap leak shows: what grows from capture to capture and does not go away.alloc_space(the default forallocs) — everything that was allocated. This is where you hunt pressure on the collector: who creates work for garbage collection even while holding nothing.
But inuse_space is not a universal leak detector:
inuse_objects— the same view in counts rather than bytes: a leak of many small objects can get lost in bytes;- a goroutine leak is the goroutine profile (Mechanism 5): in the heap profile it shows at most indirectly, through what those goroutines hold;
- memory outside the Go heap — allocated via cgo, by the kernel, mapped
into memory — the Go heap profile does not show at all. If the process grows
and
inuse_spacedoes not, that is where to look.
The heap profile is a sample too: on average one allocation per
runtime.MemProfileRate = 524288 bytes. A small allocation made once may not
make it into the profile, while a frequent one gets in in proportion to its
size. And a note on units: "MB" in pprof's output is 2²⁰ bytes, that is, MiB.
Mechanism 4: the profiles that are off by default
Four goroutines fight over one mutex — the contention is real. The block and mutex profiles:
4) BLOCK AND MUTEX PROFILES BY DEFAULT
──────────────────────────────────────
unconfigured: block 0 records, mutex 0 records
after SetBlockProfileRate(1) and SetMutexProfileFraction(1):
block 3 records, mutex 2 records
Unconfigured, both are empty. This is the most common trap: a service with
net/http/pprof, you open /debug/pprof/block — empty, and you conclude
"there is no blocking".
They answer different questions:
- block — where and for how long goroutines waited on synchronisation
primitives: channels, mutexes,
WaitGroup. The stack is of the one waiting; - mutex — where there was contention on a mutex, and the stack of the
one holding it, at
Unlock: where to shorten the critical section.
They are turned on like this:
To include every blocking event in the profile, pass rate = 1. To turn off
profiling entirely, pass rate <= 0.
A value of 1 — "record everything" — is convenient for debugging, but not free. On extreme workloads where almost all the work is blocking or locking:
1) BLOCK PROFILE: SetBlockProfileRate(1)
────────────────────────────────────────
series off "all" ratio
1 ping-pong, 200,000 exchanges 88.7 ms 145.1 ms x1.64
2 ping-pong, 200,000 exchanges 82.5 ms 146.6 ms x1.78
3 ping-pong, 200,000 exchanges 75.1 ms 142.8 ms x1.90
2) MUTEX PROFILE: SetMutexProfileFraction(1)
────────────────────────────────────────────
series off "all" ratio
1 4 goroutines × 200,000 locks 16.5 ms 25.1 ms x1.52
2 4 goroutines × 200,000 locks 33.7 ms 54.1 ms x1.61
3 4 goroutines × 200,000 locks 29.2 ms 39.3 ms x1.35
The run of bench/goprof/interfere.go: ×1.64–1.90 for blocking and ×1.35–1.61
for mutexes. On a service where blocking is a small part of the work the share
will be smaller, but "every event" mode in production is a deliberate decision,
not a default. Usually you set a larger value and get a sample.
And a distinction worth knowing. In go test the -blockprofile flag turns
recording on by itself: if -blockprofilerate is not given, every blocking
event is recorded. A service has nothing like that — the call to
runtime.SetBlockProfileRate must be in your code.
Mechanism 5: the goroutine profile — and why one snapshot is not enough
A hundred goroutines wait on a channel nobody will ever write to:
5) THE GOROUTINE PROFILE FINDS A LEAK
─────────────────────────────────────
goroutines before: 1, after: 101
in the profile: 100 goroutines with the same stack:
main.main.func2 internals.go:246
The goroutine profile is not a sample but a snapshot of every goroutine at once, grouped by stack. One snapshot shows a suspicious accumulation: a hundred goroutines on the same operation. But a hundred goroutines may be honestly waiting for work — a pool of handlers looks the same. What proves a leak is the dynamics:
snapshot A → wait → snapshot B
the same stack has grown and has not come back downThe line internals.go:246 is where they wait; that is where you look for the
cause.
Why goroutines leak, and why -race does not catch such a leak, is covered in
the lesson on channels. Here is the tool: /debug/pprof/goroutine?debug=1 in a
running service, and a comparison of two captures a few minutes apart.
Deeper: what profiling costs and who can reach /debug/pprof
The same workload — 400 calls of handle — with the CPU profile and without,
three series:
6) THE CPU PROFILE'S COST TO THE PROGRAM ITSELF
───────────────────────────────────────────────
series 400 calls without profile with CPU profile overhead
1 228.6 ms 219.4 ms -4.0%
2 223.3 ms 223.5 ms 0.1%
3 222.1 ms 221.0 ms -0.5%
The overhead changes sign from series to series: it is smaller than the
measurement noise. The reason is the same rate: a hundred stack snapshots a
second are a small share of the work next to the 800,000 sha256 calls that 400
calls of handle make in ≈222 ms.
Hence the practical distinction people ask about: the CPU profile is
usually lightweight enough for a limited capture in production — on request,
via /debug/pprof/profile (30 seconds by default; ?seconds=N sets the
duration). And -race, which on the measured workload cost ×4.8–5.0 in memory
and ×7.6–12 in time, is not kept on across the fleet (that is covered in the
lesson on data races).
And these numbers have a boundary: the measurement was taken with GOMAXPROCS=2 on one go1.24.7 workload. No overhead above noise was found on it — that is a measurement result, not a universal number: the cost depends on the workload.
The tool is not neutral. Profiles affect both the program and each other. A memory profile that records every allocation itself becomes a line in the CPU profile:
3) MEMORY PROFILE: MemProfileRate = 1
─────────────────────────────────────
series off "all" ratio
1 2,000,000 allocations of 64 B 67.0 ms 3488.9 ms x52.03
2 2,000,000 allocations of 64 B 59.8 ms 3734.4 ms x62.43
3 2,000,000 allocations of 64 B 71.4 ms 3599.8 ms x50.43
share of runtime.mProf_Malloc in the CPU profile of the same workload (cum%):
MemProfileRate by default: not in the profile
MemProfileRate = 1: 76.64%
The workload is deliberately extreme — nothing but small allocations — but the
conclusion is general: a CPU profile taken with MemProfileRate = 1 is 76.64%
the work of the memory profiler itself. That is why detailed modes are not all
turned on at once, and each profile is read with an allowance for what else was
on.
Who can reach /debug/pprof. This is the mandatory second half of the
advice "capture in production". Importing net/http/pprof registers handlers
on http.DefaultServeMux, and a service that serves it on its working port
serves the profiles too — with no access check at all:
the service registered only /hello and serves DefaultServeMux
GET /debug/pprof/cmdline -> 200, arguments: ["-db-password=demo-secret"]
GET /debug/pprof/goroutine?debug=2 -> 200, "file:line" lines: 53
GET /debug/pprof/heap -> 200
The run of bench/goprof/exposure.go; the password in the arguments is made
up. What leaks out is the launch arguments, stacks with source paths and memory
profiles. That is why /debug/pprof is an internal surface: a separate
listener (the example in the package documentation listens on
localhost:6060), access only from the internal network or behind
authentication, no exposure to the public Internet.
How to answer in an interview
Short answer: take a CPU profile — from a benchmark with go test -cpuprofile or from a service with net/http/pprof — and open it in go tool pprof -top. The profiler samples, so the functions where the program spends
the most time end up at the top, while short calls may not get in at all. The
rate — 100 times a second in go1.24.7 — is named as an implementation detail,
not a property of the language.
That is enough for a correct answer. What follows is what you add if the interviewer digs.
If the interviewer digs deeper
On "how does flat differ from cum" — the dispatcher example. "flat is where the processor was executing directly, cum is which call paths are responsible. A function that only hands out work has flat zero and cum almost the whole profile — I got 86.96%. You look at both: often it is cheaper to call a path less than to speed up the hot function."
On "how do you hunt a memory leak" — the two value types. "inuse_space is what
is held as of the last collection; a leak is hunted there by comparing two
captures. alloc_space is everything allocated; that is where you look for
pressure on the collector. A function that allocated six hundred megabytes and
held nothing is not visible in inuse_space at all. A goroutine leak is
hunted in the goroutine profile, and memory outside the Go heap the heap
profile will not show."
On "why is the block profile empty" — it is off. "Block and mutex are not
recorded by default: under real contention I had 0 records. You need to call
runtime.SetBlockProfileRate and SetMutexProfileFraction. In
go test -blockprofile it is turned on by itself, in a service it is not."
On a goroutine leak — the goroutine profile, two captures. "One snapshot shows an accumulation on one stack; a leak is when that stack grows from capture to capture and does not come back. The stack shows what they are blocked on."
On "can you do it in production" — the cost and access. "The CPU profile —
as a rule, yes, as a limited capture on request: on my workload the overhead
did not exceed noise, but that is not a universal number. /debug/pprof —
only on an internal listener, not outside. The whole fleet under -race — no."
Next they ask
How does a profile differ from a trace (go tool trace)?
A profile answers "where is the time spent" and adds up identical stacks, losing the order of events. A trace records events over time: when a goroutine became ready, when the scheduler ran it, when it blocked, when garbage collection was running.
Hence the division of labour: "the function is slow" — a profile; "the request is sometimes fast and sometimes slow, and the CPU is idle" — a trace, because the cause is usually waiting, and the CPU profile does not see waiting at all.
Why is the slow function not visible in the CPU profile when everything is slow?
Because the CPU profile sees only time on the processor. A goroutine waiting for a database response, a mutex or a channel does not occupy the processor and does not get into the CPU profile.
If the CPU is lightly loaded and requests are slow, you look at the block and mutex profiles — without forgetting to turn them on — or at a trace. The CPU profile in that case will honestly show that the program is doing almost nothing.
How do you compare two profiles — before and after an optimisation?
go tool pprof -diff_base before.out after.out shows the difference: what got
more expensive, what got cheaper. For benchmarks benchstat over several runs of each
variant is more convenient — it computes both the spread and whether the
difference is significant.
The main rule here is the same as in the lesson's measurements: one comparison of one run proves nothing. The CPU profile's overhead in this lesson changed sign from series to series — that is exactly what a difference that does not exist looks like.
What is PGO and what does the profile have to do with it?
Profile-guided optimisation: a CPU profile from production is placed in the
main package's directory under the name default.pgo, and go build applies
it to that package and its dependencies — the compiler optimises the hot paths
by it.
The link to the lesson is direct: it is one more reason to take a CPU profile in production, and what makes taking it there possible is its low cost. And a caveat: the profile must be representative. A profile taken on an atypical load, or one gone stale after code changes, optimises the wrong hot paths.
Common misconceptions
The profiler records every function call
It samples: the CPU profile in go1.24.7 — 100 times a second, hard-coded in the runtime/pprof source. In the run — 203 samples over 2.11 s. A function that ran for a few milliseconds may not get into the profile.
The most expensive function is the one at the top by cum
The top by cum also holds dispatchers that do nothing themselves: handle has flat 0 and cum 86.96%. flat is where the processor was executing directly; cum is which call paths are responsible for it. You need both.
If a function allocates a lot, it is visible in the heap profile
Only in alloc_space. In inuse_space — the default sample for heap — a function that allocated 596.83 MB and held nothing is entirely absent.
An empty block profile means there is no blocking
The block and mutex profiles are off by default: under real contention — 0 records; after SetBlockProfileRate(1) and SetMutexProfileFraction(1) — 3 and 2.
Profiling in production is too expensive
Not necessarily. On our workload the CPU profile gave an overhead of −4.0%, +0.1% and −0.5% — within noise; that is not a universal number, but a limited capture on request is usually affordable. What is expensive is -race and "record everything" modes: MemProfileRate = 1 slowed an extreme workload down 50–62 times.
If inuse_space is not growing, there is no leak
There is no Go heap leak. Goroutines leak in the goroutine profile, and memory allocated via cgo or by the kernel does not get into the Go heap profile at all.
/debug/pprof is safe to leave on the working port — it is just statistics
In the run, with no access check at all, it served the launch arguments with a password, stacks with source paths and a memory profile. It is an internal surface: a separate listener and a closed network.
The CPU profile will show why a request is slow
Only if the request is slow because of computation. Waiting for a database, a mutex or a channel does not occupy the processor and does not get into the CPU profile. For waiting — the block and mutex profiles, or a trace.
Practice
Two problems. Answer first, then check against the real output: in both, the correct answer is taken from a run of the script rather than assigned.
Practice · predict the output
fmt.Println(pprof.Lookup("block").Count())
before := pprof.Lookup("goroutine").Count()
never := make(chan int)
for i := 0; i < 100; i++ {
go func() { <-never }()
}
time.Sleep(50 * time.Millisecond)
fmt.Println(pprof.Lookup("goroutine").Count() - before)
churn(20000)
runtime.GC()
fmt.Println(strings.Contains(string(out), "main.churn"))Practice · estimate
Knowledge check
The handle function only calls two others and does nothing itself. What does it have in the CPU profile?
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- The Go profiler samples: it does not count every call but looks at some rate at what is running right now. That is the interview answer. The rate is an implementation detail: in
runtime/pprofof go1.24.7 the CPU profile takes 100 samples a second, and the number is hard-coded in the source. Measured: 203 samples over 2.11 s of one goroutine's work. Hence the main rule for reading a profile: it is a sample, and a function that ran for 5 ms may not appear in it at all. - Beyond that comes what separates real understanding from having read about it. The output of
go tool pprofhas two time columns: flat — time in the function itself, cum — together with what it calls. A dispatcher function has flat 0 and cum 1800 ms — 86.96% of the profile. The heap profile has two different value types: inuse_space — what is held right now, and alloc_space — what was allocated over all time. A function that allocated 596.83 MB and held nothing is absent frominuse_space: a leak is hunted in one value type, pressure on the collector in the other. - Two profiles are off by default. The block and mutex profiles under real contention gave 0 records until they were turned on with
SetBlockProfileRateandSetMutexProfileFraction. An empty profile does not mean "no blocking". The goroutine profile shows a suspicious accumulation: a hundred goroutines waiting on a channel nobody writes to sit under one stack; what proves a leak is that this stack keeps growing from capture to capture. The CPU profile is usually lightweight enough for a limited capture in production: on our workload the overhead did not exceed measurement noise, but that is not a universal number. And/debug/pprofis an internal surface: it is not exposed to the outside.
In fact
- It samples: the CPU profile in go1.24.7 — 100 times a second, hard-coded in the
runtime/pprofsource. In the run — 203 samples over 2.11 s. A function that ran for a few milliseconds may not get into the profile. - The top by cum also holds dispatchers that do nothing themselves:
handlehas flat 0 and cum 86.96%. flat is where the processor was executing directly; cum is which call paths are responsible for it. You need both. - Only in
alloc_space. Ininuse_space— the default sample forheap— a function that allocated 596.83 MB and held nothing is entirely absent. - The block and mutex profiles are off by default: under real contention — 0 records; after
SetBlockProfileRate(1)andSetMutexProfileFraction(1)— 3 and 2. - Not necessarily. On our workload the CPU profile gave an overhead of −4.0%, +0.1% and −0.5% — within noise; that is not a universal number, but a limited capture on request is usually affordable. What is expensive is
-raceand "record everything" modes:MemProfileRate = 1slowed an extreme workload down 50–62 times. - There is no Go heap leak. Goroutines leak in the goroutine profile, and memory allocated via cgo or by the kernel does not get into the Go heap profile at all.
- In the run, with no access check at all, it served the launch arguments with a password, stacks with source paths and a memory profile. It is an internal surface: a separate listener and a closed network.
- Only if the request is slow because of computation. Waiting for a database, a mutex or a channel does not occupy the processor and does not get into the CPU profile. For waiting — the block and mutex profiles, or a trace.
What is covered
- What is really being asked
- Base: a profile is a sample, not a full count
- Mechanism 1: the CPU profile — a sample, in go1.24.7 100 times a second
- Mechanism 2: flat and cum
- Mechanism 3: the heap profile — two different value types
- Mechanism 4: the profiles that are off by default
- Mechanism 5: the goroutine profile — and why one snapshot is not enough
- Deeper: what profiling costs and who can reach /debug/pprof
- How to answer in an interview
- Next they ask
- Common misconceptions
- Practice
- Knowledge check
Sources & further reading
4 SOURCES
- The runtime/pprof packageOfficial documentation. Which profiles exist: «goroutine - stack traces of all current goroutines», «heap - a sampling of memory allocations of live objects», «allocs - a sampling of all past memory allocations», «block - stack traces that led to blocking on synchronization primitives», «mutex - stack traces of holders of contended mutexes». And on the heap profile: «The heap profile reports statistics as of the most recently completed garbage collection».https://pkg.go.dev/runtime/pprof
- runtime/pprof source — StartCPUProfile, go1.24.7Official documentation. Where the CPU profile's rate comes from and why StartCPUProfile does not take it: «100 Hz is a reasonable choice: it is frequent enough to produce useful data, rare enough not to bog down the system, and a nice round number to make it easy to convert sample counts to seconds». And then: «Instead of requiring each client to specify the frequency, we hard code it». Right after the comment —
const hz = 100.https://github.com/golang/go/blob/go1.24.7/src/runtime/pprof/pprof.go - The runtime package — MemProfileRate, SetBlockProfileRate, SetMutexProfileFractionOfficial documentation. Why the heap profile is a sample: «The profiler aims to sample an average of one allocation per MemProfileRate bytes allocated»; the default is 512 * 1024. How to turn on the block profile: «To include every blocking event in the profile, pass rate = 1. To turn off profiling entirely, pass rate <= 0».https://pkg.go.dev/runtime#SetBlockProfileRate
- The net/http/pprof packageOfficial documentation. How profiles usually get into a service: «The package is typically only imported for the side effect of registering its HTTP handlers. The handled paths all begin with /debug/pprof/».https://pkg.go.dev/net/http/pprof