The thing nobody tells you about context.Context is that the hard part isn't understanding it, it's resisting the urge to short-circuit it. You know the rule: pass it as the first argument, name it ctx, never store it in a struct. Easy. Then you're three layers deep in some helper that needs to make an HTTP call, you don't have a ctx in scope, and there it is, the seductive context.Background(), right where you need it.
Don't. The moment you call context.Background() halfway down the stack, you've cut the wire. The deadline from the inbound request, the cancellation when the client hangs up, the trace span, all of it stops at that line. Your shiny new background context knows nothing and will happily let that goroutine run until the heat death of the universe, or at least until the connection pool is exhausted, which arrives sooner.
The fix is dull and it is the right one: thread the real context through. If the helper needs a ctx, give the helper a ctx parameter, even if that means editing six function signatures to do it. Yes, the diff is ugly. Yes, it feels like make-work. But cancellation only works if the chain is unbroken from the top of the request to the bottom, and the one place it's broken is the one place it'll leak.
I now treat context.Background() and context.TODO() as smells anywhere except main, a test, or the top of a long-lived worker. grep -n 'context.Background()' in the middle of a package is usually me finding a bug I haven't hit yet.