skip to content

How do you dump every goroutine's stack from a running Go service over HTTP?

level: juniorimportance: must knowfreq 60%

answer

  1. a blank import wires up the handlers
  2. one endpoint per profile under /debug/pprof/
  3. a query parameter picks the output format
  4. debug=2 is the panic-style full dump
  5. a census, not a sample

basics

~10 s

Import net/http/pprof for its side effect and serve HTTP on a debug port, then fetch /debug/pprof/goroutine?debug=2. That prints the full current stack of every goroutine in the process, one block each.

solid answer

~40 s

A blank import of `net/http/pprof` registers the profile handlers on `http.DefaultServeMux` from its `init`, so you only need a listener — typically `go http.ListenAndServe("localhost:6060", nil)` on an internal port. The goroutine profile then answers on three formats. With no `debug` parameter it returns a gzipped protobuf profile meant for `go tool pprof`. With `?debug=1` it returns text: one entry per distinct stack with a count of how many goroutines share it, under a `goroutine profile: total N` header. With `?debug=2` it returns the panic-style dump — every goroutine printed individually with its id, its parked state and its whole stack. The profile is a complete snapshot, not a sample, so collecting it briefly stops the world and the `?debug=2` body grows with the goroutine count.

code

go · 8 lines
go
import _ "net/http/pprof" // registers handlers on http.DefaultServeMux

func startDebugServer() {
	go func() {
		// internal-only address, never the public listener
		log.Println(http.ListenAndServe("localhost:6060", nil))
	}()
}

go deeper

for a junior

Be ready to name the endpoint and the blank import without hesitating: import net/http/pprof, then fetch /debug/pprof/goroutine?debug=2. Interviewers use this as a quick check that you have actually looked inside a live Go process.

for a middle

Explain what each debug level returns and why the default form is binary. Expect to be asked which form you would feed to go tool pprof and which one you would read by eye during an incident.

for a senior

Show that you think about where the debug listener is bound and what collecting a full dump costs on a process with a hundred thousand goroutines. Say how you would capture it mid-incident without making the incident worse.

for a principal

Own the standing decision: whether every service ships with the pprof endpoints wired to an internal-only port by default, and what the team gives up the day an incident starts on a binary that was built without them.

## What the goroutine profile is Every other pprof profile is a sample: the CPU profile records stacks at ~100 Hz, the heap profile records roughly one allocation in every 512 KB. The goroutine profile is different — it is a **census**. When you ask for it, the runtime takes a snapshot of every goroutine that exists at that instant and records its call stack. Nothing is estimated and nothing is extrapolated, which is exactly why it is the right instrument for a leak: a leaked goroutine is one that exists and should not, and a census counts existence directly. ## Wiring it up The endpoints come from the standard library package `net/http/pprof`. You import it for its side effect only: ```go import _ "net/http/pprof" ``` Its `init` registers handlers on `http.DefaultServeMux`: `/debug/pprof/` (the index), `/debug/pprof/cmdline`, `/debug/pprof/profile` (CPU), `/debug/pprof/symbol`, `/debug/pprof/trace`, and a handler for each named runtime profile, including `goroutine`. All you supply is something listening: ```go go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() ``` Two details matter in production. First, this binds the diagnostics to the **default** mux; if your service serves its real traffic on its own `http.ServeMux`, the pprof handlers are not on it, and that is usually what you want — a separate internal listener keeps the endpoints off the public surface. If you do want them on your own mux, register them explicitly with `pprof.Index` and `pprof.Handler("goroutine")` from `net/http/pprof`. Second, wire this in *before* you need it. There is no way to attach the endpoints to a process that was built and started without them, and an incident is a bad time to ship a new binary. ## The three formats The same endpoint, `/debug/pprof/goroutine`, returns three different things depending on the `debug` query parameter. **No parameter** returns a gzipped protobuf profile. This is the machine form — save it to a file and open it with `go tool pprof`, which gives you a top list, a graph and a flame view, and, crucially, lets you compare two of them. **`?debug=1`** returns text aggregated by identical stack. It opens with a line like `goroutine profile: total 12043`, then one entry per distinct stack, prefixed by the number of goroutines sitting on it. This is the fastest way to see the shape of a process: a healthy service has a long tail of small counts and a few structural stacks; a leaking one has a single stack with a count in the thousands. **`?debug=2`** returns the full dump — every goroutine printed on its own, in exactly the format the runtime uses when an unrecovered panic kills the process. Each block begins with the goroutine's id and its parked state in brackets, then the stack innermost frame first, then a `created by` line naming the function that ran the `go` statement. This is the form you read by eye when you already know something is wrong and want to see *what* the goroutines are waiting on. ## What it costs Collecting the profile requires the runtime to briefly stop the world so it can take a consistent snapshot of every goroutine's state; the stacks themselves are then copied out. On an ordinary service this is unnoticeable. On a process holding hundreds of thousands of goroutines it is not free, and the `?debug=2` body is proportional to that count — a text response measured in hundreds of megabytes is entirely possible, which is its own kind of outage if you pipe it somewhere that cannot take it. The practical posture is: bind the debug listener to localhost or an internal-only address, do not scrape `?debug=2` on a timer the way you would scrape a metric, and prefer the cheap aggregated `?debug=1` view or the protobuf form when you only need counts. ## Where it fits The goroutine profile answers "how many goroutines exist and what are they all doing?" That question is unrelated to how much memory the process is using, which is why a goroutine leak can run for days behind a perfectly flat heap graph. When a Go service misbehaves in a way that memory profiles do not explain — climbing latency, exhausted connections, a slow slide toward unresponsiveness — this endpoint is the first place to look, and it takes one HTTP request.

  • What exactly does the blank import of net/http/pprof do?
    Its `init` registers handlers on `http.DefaultServeMux` — the `/debug/pprof/` index, `cmdline`, `profile`, `symbol`, `trace`, and one handler per named runtime profile such as `goroutine` and `heap`. Nothing else in your code references the package, which is why the import is blank. If your service serves on its own mux, those handlers are not there and you must register `pprof.Index` and `pprof.Handler("goroutine")` yourself.
  • Is it safe to hit ?debug=2 on a busy production process?
    Usually, but not casually. Collecting the profile briefly stops the world to snapshot every goroutine, and the `?debug=2` body is proportional to the goroutine count, so a process holding hundreds of thousands of them can return a response measured in hundreds of megabytes. Pull it deliberately during an investigation, bind the listener to an internal address, and use the aggregated `?debug=1` view when you only need counts.
  • How is the goroutine profile different from the CPU profile in what it measures?
    The CPU profile is sampled over a window — it tells you where time is spent. The goroutine profile is a complete instantaneous census of goroutines that exist and where each is parked, with no sampling and no duration. That makes it useless for finding hot code and ideal for finding things that exist and should not, which is what a leak is.

saying these in an interview costs you the question

  • Thinks the goroutine profile is sampled like the CPU profile
  • Believes a CPU profile must be running first
  • Says the process must be restarted with a flag to collect it
  • Confuses debug=1 aggregated counts with debug=2 per-goroutine stacks
  • Exposes /debug/pprof on the public listener without a thought
  • Assumes importing net/http/pprof also starts a server