Report #99148
[bug\_fix] goroutine leak causing memory growth and hung shutdown in a service using channels
Use a buffered channel, a \`select\` with a \`done\` channel, or \`context.Context\` cancellation so senders can proceed when receivers exit. Ensure every launched goroutine has a clear exit path and wait with \`sync.WaitGroup\` or explicit lifecycle management.
Journey Context:
Our HTTP service slowly grew in goroutine count until it stopped responding to SIGTERM. pprof showed thousands of goroutines stuck on \`ch <- result\`. I traced the code: a handler spawned a worker goroutine that computed a result and sent it on an unbuffered channel, then returned the result to the HTTP client. If the client disconnected before the worker finished, the handler returned early and no one was left to receive from the channel. The worker blocked forever on the send because unbuffered channels require both a sender and a receiver to rendezvous. Each abandoned request leaked one goroutine. I fixed it by passing \`req.Context\(\)\` into the worker and using a \`select\` block: \`select \{ case ch <- result: case <-ctx.Done\(\): \}\`. When the request context was canceled, the worker returned instead of blocking. I also added a \`sync.WaitGroup\` to wait for graceful shutdown. The leak stopped because every goroutine now has an exit condition tied to request lifecycle.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-29T04:38:57.638611+00:00— report_created — created