Why is Go's block profile empty until you call runtime.SetBlockProfileRate?
answer
- not every pprof profile is on
- importing the handlers is not enough
- two runtime setters, both default zero
- the runtime charges the blocking path
- SetBlockProfileRate and SetMutexProfileFraction
basics
~20 sGo's block and mutex profilers are switched off by default because recording costs throughput. Until you call runtime.SetBlockProfileRate for blocking events, or runtime.SetMutexProfileFraction for lock contention, the runtime samples nothing and the profile comes back with no samples.
solid answer
~40 sUnlike the heap profile, which the runtime samples for you, the block and mutex profiles are opt-in. `runtime.SetBlockProfileRate(rate)` turns on sampling of goroutines blocking on synchronisation — channel operations, `select`, mutex acquisition, `sync.WaitGroup.Wait`, `sync.Cond.Wait` — and `runtime.SetMutexProfileFraction(n)` turns on sampling of contention on `sync.Mutex` and `sync.RWMutex`. Both default to zero, meaning off, because recording costs a timestamp around blocking operations plus a stack capture per sampled event, and a service that blocks millions of times a second would pay for it continuously. You set them once early in `main` (or from an admin handler), then read the results through `runtime/pprof.Lookup("block")` and `Lookup("mutex")`, or over HTTP at `/debug/pprof/block` and `/debug/pprof/mutex`.
go deeper
Remember the two setter names and that both default to zero, meaning off. Be ready to say that registering the pprof HTTP handlers does not by itself start recording blocking or contention events.
Explain what each profiler actually records and why the runtime refuses to enable them for everyone: the cost is a timestamp plus a stack capture charged to each blocking operation, so it scales with how often goroutines block.
Show that you would enable them deliberately — a coarse rate, on a canary instance, ideally flippable at runtime through an admin endpoint — and that you read deltas between two captures rather than the process-lifetime totals.
Own the default for the fleet. Decide whether the diagnostic value of always-on contention data justifies its throughput cost for your service mix, write the measured numbers down, and name who can overrule the setting.
## The short version Go's runtime can produce several different profiles, and they do not all behave the same way. The CPU profile has to be started and stopped around a window of time. The heap profile is sampled continuously with no action from you. The **block profile** and the **mutex profile** are different again: they are *permanently off* until your program explicitly enables them, and if you never do, the profile still exists — it is just empty. That is the whole reason a first attempt at `/debug/pprof/block` returns nothing useful. ## The two switches ```go runtime.SetBlockProfileRate(rate int) runtime.SetMutexProfileFraction(rate int) int ``` - `runtime.SetBlockProfileRate` controls the **block profile**, which records goroutines *waiting* on synchronisation: a send or receive on a channel that cannot proceed, a `select` with no ready case, acquiring a `sync.Mutex` that someone else holds, `sync.WaitGroup.Wait`, `sync.Cond.Wait`. Passing a rate of 0 or less leaves it off. A positive rate turns it on. - `runtime.SetMutexProfileFraction` controls the **mutex profile**, which records *contention events* on `sync.Mutex` and `sync.RWMutex` — the moments where one goroutine actually had to wait for another to release a lock. A rate of 0 turns it off; a positive rate turns it on. Both start at zero in every Go program. Importing `net/http/pprof` registers the HTTP handlers that will serve the profiles, but it does not flip either switch — this is the single most common source of the "my block profile is empty" question. ## Why they are off by default Because the cost is real and it is paid by the hot path, not by the profiler. When block profiling is enabled, the runtime reads a timestamp around blocking operations so it knows how long the goroutine was parked, and for each *sampled* event it walks the stack to record where the block happened. A stack capture is not free. In a program whose goroutines block millions of times a second — a pipeline of small channel sends, a shared counter behind a mutex — that overhead is charged to every one of those operations. Mutex profiling is cheaper in practice because contention events are usually far rarer than blocking events, but the shape of the cost is the same: it scales with **how often the thing happens**, not with how long the process runs. A mostly CPU-bound service pays almost nothing; a chatty concurrent one can pay several percent. CPU and heap profiling, by contrast, have a cost bounded by a fixed sampling interval, which is why the runtime is willing to leave heap sampling on for everyone. ## Turning them on ```go func main() { runtime.SetBlockProfileRate(10000) // one sample per 10µs blocked runtime.SetMutexProfileFraction(100) // about 1 in 100 contention events // ... start serving } ``` The calls are global and take effect immediately, so you can also expose them behind an authenticated admin endpoint and raise the rate on one instance while an incident is happening, without a redeploy. That is usually the better production design: coarse or off by default, dialled up on demand. Passing a non-positive rate to `runtime.SetBlockProfileRate` stops new events from being recorded. It does not erase what was already collected — both profiles accumulate over the life of the process, so a long-running service's raw profile is a lifetime total, and you normally want to compare two captures taken a known interval apart rather than read the absolute numbers. ## Reading them Once enabled, either fetch them through the standard library directly: ```go pprof.Lookup("block").WriteTo(w, 0) pprof.Lookup("mutex").WriteTo(w, 0) ``` or let `net/http/pprof` serve them at `/debug/pprof/block` and `/debug/pprof/mutex` and point `go tool pprof` at the result. Both profiles report two values per stack: a **contentions** count (how many events) and a **delay** in nanoseconds (how much time was spent waiting). Sorting by delay tells you where the waiting is; sorting by count tells you whether it is many short waits or a few long ones. ## What a candidate should take away The empty profile is not a bug and not a build-tag problem. It is a deliberate default: Go will not charge every program for diagnostics that only some programs need. Two lines of code, chosen deliberately rather than set to the maximum, turn it into data.
- Does a blank import of net/http/pprof enable block profiling for you?No. The blank import only registers the HTTP handlers that serve profiles. The block and mutex profilers stay at rate zero until your code calls `runtime.SetBlockProfileRate` or `runtime.SetMutexProfileFraction`, so `/debug/pprof/block` will answer with a valid but sample-free profile.
- What happens to already-collected samples if you set the block profile rate back to zero?Recording of new events stops; what was already sampled stays in the profile for the life of the process. Both profiles are cumulative counters rather than a rolling window, which is why you normally take two captures and compare, instead of reading the absolute totals.
- Where in a service would you put those calls?Early in `main`, before you start serving, so the numbers cover the whole run. Because both setters are global and take effect immediately, it is also worth exposing them behind an authenticated admin route so on-call can raise the rate on one instance mid-incident without redeploying.
It is like a flight recorder that ships switched off: the wiring and the readout port are already installed, but nothing is written to tape until someone deliberately arms it, because the tape costs fuel.
saying these in an interview costs you the question
- Thinks importing net/http/pprof enables block profiling
- Believes every Go profile is collected by default
- Says the block profile shows CPU time spent inside locks
- Sets the block profile rate to 1 in production without measuring
- Assumes a build flag or GODEBUG setting is needed instead of a runtime call