Golang <-time.After() is not garbage collected before expiry

Recently I was investigating a memory leak problem in a Go application that boiled down to me not reading the documentation properly. Here’s a piece of code that caused memory consumption of multiple Gbs:

Below is the memory metrics graph from the app:

On the left side one can see the memory consumption before the fix, and on the right — after. The profiler showed that the <-time.After was the reason for the memory leak. I was really surprised until I read the doc which says:

The underlying Timer is not recovered by the garbage collector until the timer fires.

So the 9Gb of RAM that was periodically GCed suddenly made sense. We have 60K messages per second in the channel that would be about 18 million timers allocated at every given moment plus some indeterminate number waiting to be GCed.

Trivial refactoring helped to reduce memory consumption by a factor of 20.