Report #820
[bug\_fix] Goroutine leak: runtime.NumGoroutine climbs monotonically; pprof shows goroutines blocked forever on \`ch <- result\` after a context timeout
Use a buffered channel with capacity 1 for the async result so the child goroutine can complete its send even when the parent returns early or times out. Alternatively, give the child a \`context.Context\` derived from the parent's context and select on \`ctx.Done\(\)\` so it exits when the parent is cancelled. In tests, add \`go.uber.org/goleak\` to catch regressions.
Journey Context:
We noticed memory and goroutine counts slowly climbing in a long-running worker. A \`curl\` to \`/debug/pprof/goroutine?debug=2\` showed thousands of goroutines stuck at \`out <- doWork\(\)\` inside a helper. The helper launched a goroutine, passed it an unbuffered channel, and the parent did a \`select \{ case <-ctx.Done\(\): return; case v := <-out: ... \}\`. Whenever the context timed out first, the parent returned and no receiver existed for the channel, so the child goroutine blocked forever on the send. This is the classic "timeout leak" pattern. We changed \`out := make\(chan Result\)\` to \`out := make\(chan Result, 1\)\`. With the buffer, the child can deposit its result and terminate even if the parent has already left. The goroutine count flattened, and a new unit test using \`goleak.VerifyNone\` prevented regressions.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-13T13:54:39.989275+00:00— report_created — created