Go 1.27's goroutine leak profile: a useful signal, not a verdict
Go 1.27 can report a class of permanently blocked goroutines through pprof. Here is what the new profile detects, how to collect it safely, and where it still misses leaks.
A goroutine count is a warning, not a diagnosis
A service can have thousands of goroutines without having a leak. Requests may be waiting on a dependency, workers may be queued behind a rate limit, or a traffic spike may have created a temporary backlog. A rising count is worth investigating, but it does not tell you which goroutines are permanently stuck.
The regular goroutine profile gives you stack traces for the goroutines that exist at the moment you collect it. That is useful, but the stacks still need interpretation: a large group waiting on a channel might be healthy work, or it might be work that has lost every possible path to completion.
What changed in Go 1.27
Go 1.27, released on 19 August 2026, makes the goroutine leak profile generally available. It first appeared as an experiment in Go 1.26. The profile is named goroutineleak in runtime/pprof and is exposed by net/http/pprof at /debug/pprof/goroutineleak.
This is a useful version detail: the Go 1.26 experiment required GOEXPERIMENT=goroutineleakprofile. Go 1.27 removes that experiment setting because the profile is now part of the release. Do not carry the old experiment flag into a Go 1.27 build.
What the runtime can identify
The runtime looks for a specific kind of dead end. A goroutine is blocked on a concurrency primitive, and that primitive is no longer reachable from any runnable goroutine or from another goroutine that could eventually unblock it. The garbage collector already tracks object reachability; the leak profile uses that information to distinguish a class of blocked goroutines that cannot make progress.
The supported cases include channel sends and receives, blocking select statements, and the synchronization primitives Mutex, RWMutex, WaitGroup, and Cond. That is a meaningful set: abandoned channel sends and waits on synchronization objects are common ways for goroutines to outlive the operation that created them.
- The profile is not simply a list of goroutines that have been blocked for a long time.
- It reports goroutines the runtime can classify as permanently blocked under its reachability model.
- The regular goroutine profile remains useful for investigating all current goroutines, including those outside this narrower category.
Collect a profile through a private debug listener
If a service already installs the net/http/pprof handlers, the new endpoint is available without a separate profiling library. For a production service, I would keep profiling on a private debug listener or an authenticated, network-restricted administration path. Do not expose pprof endpoints on an unrestricted public application listener.
Once the endpoint is reachable from your workstation or a controlled debugging environment, collect a snapshot and open it with the Go pprof tool:
curl -fsS http://127.0.0.1:6060/debug/pprof/goroutineleak -o goroutineleak.prof
go tool pprof goroutineleak.prof
(pprof) top
(pprof) list YourFunctionHow to read the result
Start with the stacks that account for the largest number of leaked goroutines, then inspect the blocking operation in the relevant function. The stack tells you where the goroutine stopped; it does not automatically explain why the owner failed to release it.
Trace the lifetime backwards. Which request, job, or component created the goroutine? What event was supposed to wake it? Could the parent return early, stop reading results, or shut down without closing a channel or releasing a synchronization primitive? The fix is usually in the ownership and shutdown path, not in the profiler.
- Compare the leak profile with the regular goroutine profile and the service's traffic and queue metrics.
- Look for repeated stacks across snapshots rather than treating one snapshot as a complete incident timeline.
- After fixing the lifecycle, add a regression test that exercises the early-return or shutdown path.
The important blind spots
A clean goroutine leak profile does not prove that a service has no goroutine leaks. The detector is intentionally limited to the concurrency primitives it understands. Goroutines blocked in file or network I/O, direct system calls, or custom synchronization mechanisms are outside that detection set unless the custom mechanism is built on supported primitives.
Reachability can also hide a leak. If a blocked primitive remains reachable through a global variable or a runnable goroutine, the runtime may not report the goroutine even when the primitive will never be used again. The official Go article describes these as cases the profiler can miss, so treat the output as strong evidence when it finds a leak—not as a proof of absence when it finds none.
The detector also cannot predict a leak before it happens. A flaky timing bug still needs to be reproduced or observed after the blocked state occurs.
There is a cost to proving a leak
The implementation reuses garbage-collector reachability, so the extra memory used for bookkeeping is described as negligible. The Go team also documents a CPU trade-off: leak detection can make a garbage-collection cycle slower, and a pathological chain of goroutines and synchronization primitives can produce a worst-case quadratic amount of checking work.
That is not a reason to avoid the feature. It is a reason to treat collection frequency as an operational choice. I would begin with controlled snapshots during an investigation, measure the effect in staging, and avoid turning a diagnostic profile into a high-frequency scrape without evidence that the overhead is acceptable. This is a rollout recommendation, not a benchmark result.
Keep tests in the loop
Production profiling and deterministic tests solve different problems. The Go team's guidance recommends combining goroutine leak profiles with test-time tools such as goleak and the synctest package. Tests can catch a lifecycle regression close to the code that introduced it; a production profile can reveal behavior that only appears under real traffic, deployment timing, or shutdown conditions.
For a service with worker pools, bounded concurrency and clear cancellation ownership are still the foundation. The profile helps investigate a failure after it occurs. It does not replace designing the worker lifecycle so every goroutine has a clear way to finish.
My take
For Go 1.27 services, the goroutineleak profile is worth making available on a private diagnostic path. It answers a more precise question than a goroutine count: which goroutines are blocked on supported synchronization primitives that the runtime can prove are no longer reachable by anything able to wake them?
That is a useful new signal, but it is not a universal leak detector. Keep the ordinary goroutine profile, tests, cancellation checks, and shutdown tests. Use the new profile to narrow an investigation—not to declare a service leak-free.