Go

Does time.After in a loop leak memory? How did Go 1.23 change this?

Question 199HardGo 1.22 to 1.25
for {
	select {
	case msg := <-msgs:
		handle(msg)
	case <-time.After(time.Minute): // new timer every iteration
		log.Println("idle")
	}
}

Before Go 1.23: every iteration created a new Timer with a buffered channel, and the runtime timer heap kept it alive until it fired. With thousands of messages per second and a one-minute timeout, millions of live timers piled up: a real memory leak and CPU cost, and a classic interview question.

Go 1.23+ (when the module's go line is >= 1.23): unreferenced timers and tickers become eligible for GC right away, even if they are unstopped. Timer channels are also now synchronous (unbuffered), so Reset/Stop never return a stale value. The memory leak is gone, but you still allocate one timer per iteration.

The idiomatic fix works on every version:

t := time.NewTimer(time.Minute)
defer t.Stop()
for {
	select {
	case msg := <-msgs:
		handle(msg)
		t.Reset(time.Minute) // Go 1.23+: no drain needed
	case <-t.C:
		log.Println("idle")
		t.Reset(time.Minute)
	}
}

Before 1.23, a correct Reset required if !t.Stop() { <-t.C }, and that drain could itself deadlock if the value had already been received. Mentioning this history marks you as senior.

More on Channels & select

All 38 Channels & select questions