skip to content

What does runtime.NumGoroutine() count, and what does it deliberately leave out?

level: middleimportance: should knowfreq 52%

answer

  1. a count of what exists, not of what is running
  2. hello world does not print zero
  3. finished ones stop counting; the runtime's own never did
  4. not the same number as OS threads

basics

~20 s

runtime.NumGoroutine returns how many goroutines exist right now in any state: running, runnable, sleeping or blocked. It excludes goroutines that have already finished and the runtime's own system goroutines, and the value is a snapshot that can be stale the moment it returns.

solid answer

~40 s

`runtime.NumGoroutine()` returns the number of live user goroutines. Three properties decide how to read it. It is state-blind: a goroutine parked forever on a channel counts exactly as much as one burning CPU, so a large value means "many goroutines exist", never "the program is busy". It excludes the goroutines the runtime creates for itself — garbage-collection workers, the sweeper, the finalizer runner — which is why a program that has started none of its own prints 1, the main goroutine. And a goroutine that returns stops being counted immediately; its internal G record goes onto a free list for reuse, so the number really does fall when work completes. It is also not a thread count: OS threads are a separate, usually much smaller number, reported as `threads=` in schedtrace output.

code

go · 4 lines
go
// In a program that has started no goroutines of its own,
// this prints 1: the goroutine running main. The runtime's
// own GC, sweeper and finalizer goroutines are not counted.
fmt.Println(runtime.NumGoroutine())

go deeper

for a junior

Be able to say what the function returns and that goroutines blocked or sleeping still count. Remember that a program with no goroutines of its own still reports one, because main is itself a goroutine.

for a middle

Explain the exclusions and why they exist: system goroutines are subtracted, finished goroutines leave the count immediately as their records go onto a free list, and the value is read without locking so it is inherently a sample.

for a senior

Show that you read the number against expected in-flight work rather than against a fixed threshold, and that you know its limit — it identifies that something is wrong but never which goroutines or why, which is a different instrument.

for a principal

The judgment to own is what the number is worth as a signal: it is cheap enough to expose everywhere, so decide whether it is a paging signal or only a diagnostic one, and what evidence you expect an engineer to gather before acting on it.

## What is being counted Every goroutine is a runtime object — a G record holding its stack, its status and its bookkeeping. `runtime.NumGoroutine()` reads the runtime's counters for those records and returns how many currently belong to live user goroutines. The definition is narrower than "everything the runtime is doing", in two ways worth stating precisely. **Finished goroutines are not counted.** When a goroutine's function returns, its G is taken out of the live population and pushed onto a free list so a later `go` statement can reuse it without allocating. The record still exists — reuse is the point — but the count drops at once. You do not have to wait for a garbage collection to see a goroutine go away. **The runtime's own goroutines are not counted.** The collector's background mark workers, the sweeper, the scavenger, the finalizer runner and friends are flagged as system goroutines and subtracted out. This is why the classic demonstration works: a program that has started nothing of its own prints `1` — the goroutine running `main`. ## What it is blind to The number says nothing about state. Included in the same total are goroutines that are: - running on a P right now, - runnable and queued waiting for one, - blocked on a channel send or receive, a mutex, or a `select`, - sleeping in `time.Sleep`, - parked in the network poller waiting for a socket, - blocked in a system call. That is the property that trips people up. A gateway holding 50,000 idle client connections legitimately holds 50,000 goroutines, nearly all parked in the poller, using no CPU. A batch job with 12 goroutines can be pinning every core. The count is a *population*, not a *load*. The companion mistake is reading it as a thread count. Goroutines are multiplexed onto a much smaller set of OS threads; the thread number is separate and generally close to `GOMAXPROCS` plus a handful, and it is what `threads=` reports in `GODEBUG=schedtrace` output. Goroutines climbing while threads stay flat is ordinary. Threads climbing is a different investigation altogether. ## Snapshot semantics The function reads counters that other goroutines are mutating while it runs. It returns a value that was true at some instant inside the call, and by the time you print it the program may have created or retired thousands. There is no locking that makes it authoritative and none is wanted — it is meant to be cheap. Practically, that means: treat it as a sample; never assert exact equality on it while other goroutines are in flight; and when you compare two readings, compare them across a long enough gap that ordinary churn is small relative to the difference you care about. ## What it is good for Because it is cheap and needs no build flags or endpoints, `NumGoroutine` is the natural way to expose "how many goroutines does this process have" from inside the program — a gauge, a log line at shutdown, a number printed next to a health response. What it cannot do is tell you *which* goroutines or *why*. Once the number says something is wrong, you need a different instrument for the next question: `GODEBUG=schedtrace` to learn whether the population is runnable or parked, `scheddetail=1` to get a line per goroutine with its wait reason, or a full stack dump to see exactly where each one is stopped. `NumGoroutine` is the smoke alarm, not the investigation. ## A small trap in interpretation Because the count includes goroutines blocked in system calls and in the poller, a number that looks alarming can be entirely healthy for the workload — and a modest number can hide a leak if the process also creates and finishes thousands per second, so that a slowly-accumulating population is masked by churn. The number is evidence, not a verdict; it becomes conclusive only when read against what the program is supposed to have in flight.

  • Why is a NumGoroutine value of 50,000 not automatically a problem?
    Goroutines are cheap — a small growable stack each and no dedicated OS thread — so a server holding 50,000 idle connections legitimately holds 50,000 goroutines, nearly all parked in the network poller consuming no CPU. The number only becomes evidence when you weigh it against the work the process is actually supposed to have in flight.
  • How does NumGoroutine relate to the threads= field in a schedtrace line?
    They count different things. Goroutines are scheduled by the runtime and can number in the hundreds of thousands; `threads=` counts OS threads the runtime has created, normally close to GOMAXPROCS plus a few for blocking syscalls and the runtime's monitor. Goroutines rising while threads stay flat is normal and expected.
  • Is the value returned by runtime.NumGoroutine ever exact?
    Only in the sense that it was true at some instant during the call. It reads counters that other goroutines mutate concurrently, so it is a sample by construction. Compare readings across a meaningful interval rather than trusting any single value, and never assert exact equality on it while other goroutines are running.

It is the building's occupancy counter: it tells you how many people are inside, not how many are working, and it does not count the maintenance staff.

saying these in an interview costs you the question

  • Thinks NumGoroutine returns the number of OS threads
  • Says a high count proves the program is CPU-busy
  • Expects the garbage collector's own goroutines to be included
  • Believes finished goroutines keep counting until a collection
  • Treats the returned value as exact rather than a snapshot