Lesson 8 / 25

Timeouts, Timers and Tickers

time.After, NewTimer, NewTicker and their pitfalls.

Putting time into select

time.After(d) returns a channel that receives once after d, which makes a one-line timeout: case <-time.After(2*time.Second):. Inside a loop, however, time.After creates a new timer every iteration, so the timeout restarts each time any other case fires, and in Go versions before 1.23 each un-fired timer stayed in memory until it expired. For a timeout that spans the whole loop, create one time.NewTimer before the loop; for periodic work use time.NewTicker and always defer t.Stop(). Go 1.23 changed timers so that unreferenced timers and tickers can be garbage-collected and Reset/Stop no longer leave stale values in the channel (check the release notes for your version). In request-handling code, prefer a context.WithTimeout, which also propagates to callees.

One overall deadline plus a periodic tick

A single timer for the whole loop, a ticker for progress.

package main

import (
	"fmt"
	"time"
)

func main() {
	work := make(chan int)
	go func() {
		for i := range 5 {
			time.Sleep(150 * time.Millisecond)
			work <- i
		}
	}()

	deadline := time.NewTimer(500 * time.Millisecond) // created once
	defer deadline.Stop()
	tick := time.NewTicker(200 * time.Millisecond)
	defer tick.Stop()

	for {
		select {
		case v := <-work:
			fmt.Println("result", v)
		case <-tick.C:
			fmt.Println("still working...")
		case <-deadline.C:
			fmt.Println("overall timeout")
			// In a long-running program the producer would now be stuck
			// on its send forever: a goroutine leak (see section 8).
			return
		}
	}
}

Per-wait vs overall timeouts

time.After inside the loop means "no event for d"; a timer made once outside the loop means "the whole operation within d". Decide which one you need before writing the select.

Quick check: Why is time.After inside a busy for-select loop often a mistake for an overall timeout?

  • time.After panics after the first use
  • time.After cannot be used in select
  • A new timer is created every iteration, so the timeout restarts whenever another case fires
  • It makes the select choose cases in order
Answer

A new timer is created every iteration, so the timeout restarts whenever another case fires — Create one timer before the loop, or use a context with a deadline.