skip to content

net/http/pprof Endpoints

One blank import registers live CPU, heap and goroutine endpoints on DefaultServeMux, which is why the follow-up is always how you keep that mux off a public listener.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

What does a blank import of net/http/pprof add to a Go program, and how do you reach it?

level: juniorimportance: must knowfreq 52%

answer

  1. a side-effect import, nothing else
  2. the package's init registers handlers
  3. http.HandleFunc writes to one global router
  4. a nil handler means that same router

basics

~20 s

A blank import of net/http/pprof runs the package's init, which registers /debug/pprof handlers on http.DefaultServeMux. It starts no listener of its own, so the routes are reachable only if some server actually serves that mux.

solid answer

~40 s

Importing the package for its side effects only, `import _ "net/http/pprof"`, runs its init function, which calls `http.HandleFunc` for `/debug/pprof/`, `/debug/pprof/cmdline`, `/debug/pprof/profile`, `/debug/pprof/symbol` and `/debug/pprof/trace`. Those registrations land on `http.DefaultServeMux`, and nothing else happens: no goroutine, no port, no sampling. You reach them by serving that mux — the conventional operator listener is `go http.ListenAndServe("localhost:6060", nil)`, where the nil handler means `http.DefaultServeMux`. The index handler at `/debug/pprof/` also serves the named runtime profiles under the same prefix, so `/debug/pprof/heap` and `/debug/pprof/goroutine` work through it. The flip side matters just as much: if your public server is started with a nil handler, that same blank import has just published the debug surface to every client that can reach the port.

code

go · 12 lines
go
import (
	"log"
	"net/http"
	_ "net/http/pprof" // side effects only: registers /debug/pprof routes
)

func startOpsListener() {
	go func() {
		// a nil handler means http.DefaultServeMux
		log.Println(http.ListenAndServe("localhost:6060", nil))
	}()
}

go deeper

for a junior

Recall the chain: blank import runs init, init registers /debug/pprof routes on http.DefaultServeMux, and you still need a server serving that mux. Be able to name the loopback listener idiom on port 6060.

for a middle

Explain why the routes are sometimes missing and sometimes over-exposed from the same import: it all turns on whether anything serves http.DefaultServeMux, and a nil handler means exactly that mux.

for a senior

Show that you check the whole dependency graph, not just your own imports, and that you never let the public listener run with a nil handler in a service that has this package anywhere beneath it.

for a principal

Own the standard: whether a side-effect import may reach a global router at all in your codebase, and what the platform template gives teams so nobody has to rediscover the loopback listener pattern.

## What the import actually is Go requires every import to be used. When you want a package purely for the work its `init` function does, you import it under the blank identifier: ```go import _ "net/http/pprof" ``` That is called a blank, or side-effect, import. No symbol from the package becomes available to your code; the only thing you get is whatever the package does at initialisation time. ## What net/http/pprof does at init The package's `init` registers HTTP handlers by calling the package-level `http.HandleFunc`, which writes into `http.DefaultServeMux` — the request router that the `net/http` package keeps as a package-level global. The paths registered directly are: - `/debug/pprof/` — the index handler, `pprof.Index` - `/debug/pprof/cmdline` — the process's command line, `pprof.Cmdline` - `/debug/pprof/profile` — a CPU profile collected over a window, `pprof.Profile` - `/debug/pprof/symbol` — address-to-function-name lookup, `pprof.Symbol` - `/debug/pprof/trace` — an execution trace over a window, `pprof.Trace` The index handler does more than render a page. Any path under `/debug/pprof/<name>` is served by looking `<name>` up among the runtime's named profiles, which is how `/debug/pprof/heap`, `/debug/pprof/allocs`, `/debug/pprof/goroutine`, `/debug/pprof/threadcreate`, `/debug/pprof/block` and `/debug/pprof/mutex` all work without a registration of their own. The same package also exports `pprof.Handler(name)` if you want one of those as an `http.Handler` you place yourself. Notice what is *not* in that list: the import starts no goroutine, opens no socket, binds no port, and collects nothing. Sampling begins when a request arrives, and only for the endpoint requested. Two of those profiles — block and mutex — additionally stay empty until the program has switched their sampling on, which is a separate decision from importing this package. ## Reaching the handlers Because the handlers live on `http.DefaultServeMux`, something must serve that mux. Two things do: 1. `http.ListenAndServe(addr, nil)` — a nil handler means `http.DefaultServeMux`. Same for `http.Server{Addr: addr}` with no `Handler` set. 2. Mounting it explicitly, e.g. `http.ListenAndServe(addr, http.DefaultServeMux)`. The idiom the standard library documents is a tiny listener on loopback, started in its own goroutine so it does not block the program's real work: ```go go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() ``` Port 6060 is only a convention; nothing in the package knows about it. Once that listener is up you can point a browser at `http://localhost:6060/debug/pprof/`, or fetch a profile with the toolchain: `go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30`. ## The two failure modes, and they are opposites **Registered but unreachable.** Most non-trivial services build their own router with `http.NewServeMux()` and pass it to the server. In that program the blank import still registers everything — on a mux nobody serves. Engineers then report that "pprof does not work"; the fix is either to serve `http.DefaultServeMux` on a second listener, or to register `pprof.Index`, `pprof.Cmdline`, `pprof.Profile`, `pprof.Symbol` and `pprof.Trace` onto a mux of your own. **Registered and far too reachable.** The mirror image is the reason this import is treated as a security-relevant line. If the public listener was started with a nil handler, `/debug/pprof/` is now on the internet: anyone can read the process's command line and arguments, enumerate goroutine stacks, download a heap profile that reveals your code's structure, and force the process to spend thirty seconds of CPU collecting a profile on demand. Worse, the import does not have to be yours — any package anywhere in the dependency graph that blank-imports `net/http/pprof` has the same effect on your process, because the mux it writes to is a global. ## What an interviewer is checking That you know the mechanism rather than the recipe: side-effect import → `init` → `http.HandleFunc` → `http.DefaultServeMux` → whatever serves that mux. Everything else about pprof endpoints — why they are unreachable, why they are exposed, which listener they belong on — follows from that one chain.

  • Does the blank import start a listener for you?
    No. It only registers handlers on http.DefaultServeMux. You still have to serve that mux — commonly `go http.ListenAndServe("localhost:6060", nil)` — or register the exported handlers on a mux of your own. Nothing binds a port and nothing is sampled until a request arrives.
  • Which profiles are reachable that the package does not register a path for?
    The index handler at `/debug/pprof/` resolves any name under that prefix against the runtime's named profiles, so `/debug/pprof/heap`, `/debug/pprof/allocs`, `/debug/pprof/goroutine`, `/debug/pprof/threadcreate`, `/debug/pprof/block` and `/debug/pprof/mutex` all work. The last two return nothing until the program has turned their sampling on separately.
  • A dependency you did not write blank-imports net/http/pprof. What is the effect on your process?
    Exactly the same effect as if you had written the import yourself: its init runs during program startup and populates the same global http.DefaultServeMux. That is why an audit has to look at the whole dependency graph, not just your own files.

The import is like having a set of service panels pre-cut into a wall: they exist the moment the wall is built, but they only become usable once someone runs a corridor up to that wall — and if that corridor happens to be the public entrance, the panels are public too.

saying these in an interview costs you the question

  • Says the import starts an HTTP server on port 6060
  • Thinks the import begins collecting a CPU profile immediately
  • Believes /debug/pprof appears on every mux the program creates
  • Cannot say which mux http.HandleFunc registers on
  • Assumes only your own code's imports can register those routes
open as a page

Why does /debug/pprof/profile?seconds=30 block for thirty seconds while /debug/pprof/heap answers at once?

level: middleimportance: should knowfreq 42%

basics

~20 s

The /debug/pprof/profile endpoint turns CPU profiling on, waits out the requested window (30 seconds by default) and then streams the samples, so the request lasts the whole window. The /debug/pprof/heap endpoint needs no window and dumps current allocation records immediately.

open as a page

A Go service's internet-facing listener answers /debug/pprof/heap although no code registers that route — how, and how do you take it off?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Something in the build blank-imports net/http/pprof, whose init registers the handlers on http.DefaultServeMux, and the public server was started with a nil handler — which means exactly that mux. Give the public listener its own http.NewServeMux and serve pprof elsewhere.

open as a page

How do you decide whether net/http/pprof ships in a production build, and on which listener?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Treat it as a policy with three settings: always on a loopback or authenticated operator listener, never on a public one, and compiled out only where a debug surface is genuinely unacceptable. Decide once per class of service, not per incident.

open as a page