Go

How do goroutine leaks happen, and how do you detect them?

Question 361MediumGo 1.22 to 1.25

A goroutine blocked forever is never collected, and neither is everything its stack references. Goroutine leaks are the most common real-world Go memory leak.

// LEAK: returns after the first result; the others block on send forever
func firstLeaky(urls []string) string {
    ch := make(chan string)
    for _, u := range urls {
        go func() { ch <- fetch(u) }()
    }
    return <-ch
}

// FIX: buffered channel sized to the number of senders (or use ctx cancel)
func first(ctx context.Context, urls []string) string {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel() // tells the stragglers to stop
    ch := make(chan string, len(urls))
    for _, u := range urls {
        go func() { ch <- fetchCtx(ctx, u) }()
    }
    return <-ch
}

Typical causes: sending with no receiver, receiving from a channel that is never closed, a range over a channel whose producer never calls close, missing ctx.Done() cases, sync.WaitGroup counts that never reach zero, HTTP response bodies left unclosed, and forgotten time.Ticker loops.

Detection:

  • Watch runtime.NumGoroutine() or the /sched/goroutines:goroutines metric over time.
  • Open /debug/pprof/goroutine?debug=1 to see goroutines grouped by stack. A group whose count keeps growing is the leak.
  • In tests, call goleak.VerifyNone(t) from go.uber.org/goleak.
  • Go 1.26 adds an experimental goroutine leak profile (GOEXPERIMENT=goroutineleakprofile). It uses the GC to find goroutines blocked on concurrency primitives that nothing else can reach.

More on Memory, GC & Runtime Internals

All 38 Memory, GC & Runtime Internals questions