Skip to main content
Go’s built-in timer feature allows you to execute code at some point in the future. Timers are useful for scheduling one-time events, implementing timeouts, and creating cancellable delays.

Basic Timer Usage

package main

import (
	"fmt"
	"time"
)

func main() {
	// Create a timer that fires after 2 seconds
	timer1 := time.NewTimer(2 * time.Second)

	// Block until the timer fires
	<-timer1.C
	fmt.Println("Timer 1 fired")
}
The timer’s channel C receives a value (the current time) when the timer expires.

Timer vs Sleep

Both wait for a duration, but timers can be cancelled:
// Simple delay - cannot be cancelled
time.Sleep(2 * time.Second)

// Timer - can be cancelled
timer := time.NewTimer(2 * time.Second)
<-timer.C
Use time.Sleep() for simple delays. Use time.NewTimer() when you need cancellation capability.

Cancelling a Timer

timer2 := time.NewTimer(time.Second)

go func() {
	<-timer2.C
	fmt.Println("Timer 2 fired")
}()

// Stop the timer before it fires
stopped := timer2.Stop()
if stopped {
	fmt.Println("Timer 2 stopped")
}

// Give timer2 time to fire (to prove it was stopped)
time.Sleep(2 * time.Second)
Output:
Timer 2 stopped
The timer is cancelled, so “Timer 2 fired” is never printed.

Timer.Stop() Return Value

stopped := timer.Stop()
  • true: Timer was stopped before it fired
  • false: Timer already fired or was already stopped
Calling Stop() does not close the channel. If the timer has already fired, you may need to drain the channel to prevent blocking.

Draining a Timer Channel

if !timer.Stop() {
	// Timer already fired, drain the channel
	select {
	case <-timer.C:
	default:
	}
}

Resetting a Timer

timer := time.NewTimer(1 * time.Second)

// Reset to fire after 2 seconds instead
timer.Reset(2 * time.Second)

<-timer.C
fmt.Println("Timer fired after reset")
Reset() should only be called on stopped or expired timers. For active timers, call Stop() first.

Safe Reset Pattern

if !timer.Stop() {
	select {
	case <-timer.C:
	default:
	}
}
timer.Reset(newDuration)

Practical Patterns

1. Timeout Implementation

func fetchWithTimeout(url string, timeout time.Duration) (string, error) {
	result := make(chan string, 1)
	errors := make(chan error, 1)
	timer := time.NewTimer(timeout)
	defer timer.Stop()
	
	go func() {
		data, err := fetch(url)
		if err != nil {
			errors <- err
			return
		}
		result <- data
	}()
	
	select {
	case data := <-result:
		return data, nil
	case err := <-errors:
		return "", err
	case <-timer.C:
		return "", fmt.Errorf("timeout after %v", timeout)
	}
}

2. Debouncing User Input

func debounce(input <-chan string, delay time.Duration) <-chan string {
	out := make(chan string)
	
	go func() {
		var timer *time.Timer
		var lastValue string
		
		for value := range input {
			lastValue = value
			
			if timer != nil {
				timer.Stop()
			}
			
			timer = time.AfterFunc(delay, func() {
				out <- lastValue
			})
		}
		close(out)
	}()
	
	return out
}

3. Retry with Backoff

func retryWithBackoff(operation func() error, maxRetries int) error {
	backoff := 1 * time.Second
	
	for i := 0; i < maxRetries; i++ {
		err := operation()
		if err == nil {
			return nil
		}
		
		if i < maxRetries-1 {
			fmt.Printf("Attempt %d failed, retrying in %v\n", i+1, backoff)
			timer := time.NewTimer(backoff)
			<-timer.C
			backoff *= 2  // Exponential backoff
		}
	}
	
	return fmt.Errorf("max retries exceeded")
}

4. Rate Limiting

func rateLimiter(requests <-chan Request, rate time.Duration) {
	for req := range requests {
		processRequest(req)
		
		// Wait before processing next request
		timer := time.NewTimer(rate)
		<-timer.C
	}
}

time.After() Shorthand

For one-time use, time.After() is more convenient:
// time.After() creates and returns timer.C
select {
case result := <-ch:
	process(result)
case <-time.After(5 * time.Second):
	fmt.Println("timeout")
}

// Equivalent to:
timer := time.NewTimer(5 * time.Second)
select {
case result := <-ch:
	process(result)
case <-timer.C:
	fmt.Println("timeout")
}
time.After() cannot be cancelled and the timer won’t be garbage collected until it fires. Don’t use it in loops - use time.NewTimer() instead.

time.AfterFunc()

Execute a function after a duration:
timer := time.AfterFunc(2*time.Second, func() {
	fmt.Println("Function executed after 2 seconds")
})

// Can still be cancelled
time.Sleep(1 * time.Second)
timer.Stop()
fmt.Println("Timer cancelled")

Comparison: Timer Functions

FunctionReturnsCan CancelUse Case
time.NewTimer()*TimerYesReusable, cancellable delays
time.After()<-chan TimeNoOne-time timeout in select
time.AfterFunc()*TimerYesExecute function after delay
time.Sleep()NothingNoSimple blocking delay

Reusable Timer Pool

type TimerPool struct {
	pool sync.Pool
}

func NewTimerPool() *TimerPool {
	return &TimerPool{
		pool: sync.Pool{
			New: func() interface{} {
				return time.NewTimer(0)
			},
		},
	}
}

func (tp *TimerPool) Get(d time.Duration) *time.Timer {
	timer := tp.pool.Get().(*time.Timer)
	timer.Reset(d)
	return timer
}

func (tp *TimerPool) Put(timer *time.Timer) {
	if !timer.Stop() {
		select {
		case <-timer.C:
		default:
		}
	}
	tp.pool.Put(timer)
}

Heartbeat Pattern

func heartbeat(interval time.Duration, quit <-chan struct{}) <-chan time.Time {
	heartbeats := make(chan time.Time)
	
	go func() {
		defer close(heartbeats)
		timer := time.NewTimer(interval)
		defer timer.Stop()
		
		for {
			select {
			case <-quit:
				return
			case t := <-timer.C:
				heartbeats <- t
				timer.Reset(interval)
			}
		}
	}()
	
	return heartbeats
}

Deadline Pattern

func processWithDeadline(deadline time.Time) error {
	timeout := time.Until(deadline)
	if timeout <= 0 {
		return errors.New("deadline already passed")
	}
	
	timer := time.NewTimer(timeout)
	defer timer.Stop()
	
	result := make(chan error, 1)
	
	go func() {
		result <- doWork()
	}()
	
	select {
	case err := <-result:
		return err
	case <-timer.C:
		return errors.New("deadline exceeded")
	}
}

Common Mistakes

Memory Leak with time.After in Loop

// BAD: Creates many timers that aren't GC'd
for {
	select {
	case msg := <-messages:
		process(msg)
	case <-time.After(1 * time.Second):  // LEAK!
		timeout()
	}
}

// GOOD: Reuse timer
timer := time.NewTimer(1 * time.Second)
defer timer.Stop()

for {
	timer.Reset(1 * time.Second)
	select {
	case msg := <-messages:
		process(msg)
	case <-timer.C:
		timeout()
	}
}

Not Draining After Stop

// BAD: May block on receive
timer := time.NewTimer(1 * time.Second)
go func() {
	<-timer.C  // May block forever if stopped
	fmt.Println("Fired")
}()
timer.Stop()

// GOOD: Check stop result and drain
timer := time.NewTimer(1 * time.Second)
if !timer.Stop() {
	<-timer.C  // Drain the channel
}

Resetting Active Timer

// BAD: Resetting active timer
timer := time.NewTimer(1 * time.Second)
timer.Reset(2 * time.Second)  // Unsafe!

// GOOD: Stop before reset
timer := time.NewTimer(1 * time.Second)
if !timer.Stop() {
	<-timer.C
}
timer.Reset(2 * time.Second)

Performance Considerations

Timers are lightweight. Creating a timer costs ~100-200 ns. However, in tight loops with millions of iterations, consider timer pooling.

Benchmarks (approximate)

  • Creating timer: ~100-200 ns
  • Stopping timer: ~20-30 ns
  • Resetting timer: ~30-50 ns
  • time.Sleep(): ~1000 ns overhead

Best Practices

  1. Use time.Sleep for simple delays - No need for timer if you can’t cancel
  2. Defer timer.Stop() - Always clean up timers
  3. Avoid time.After in loops - Creates memory leaks
  4. Drain channel after Stop - Prevent blocking on already-fired timers
  5. Stop before Reset - Safer to stop and drain before resetting
  6. Use buffered channels with timers - Prevent goroutine leaks
  7. Consider context.WithTimeout - More composable for cancellation

Timer vs Ticker

FeatureTimerTicker
FiresOnceRepeatedly
Use caseOne-time eventPeriodic events
ResetChanges next fire timeChanges interval
ExampleTimeout, delayHeartbeat, polling
  • Tickers - Repeating timers for periodic events
  • Timeouts - Implementing timeouts with timers
  • Select - Using timers in select statements
  • Context - Cancellation and deadlines

Build docs developers (and LLMs) love