Timers, Tickers, Rate Limiting & Stateful Goroutines ⏱️

In cloud-native backend services, operations frequently require time coordination: periodic metric emission, heartbeat checks, connection timeouts, API rate limiting, and thread-safe state management.


1. Timers: One-Shot Delays (time.NewTimer)

A Timer represents a single event in the future. You tell the timer how long you want to wait, and it provides a channel that will be notified at that time:

package main
 
import (
	"fmt"
	"time"
)
 
func main() {
	// 1. Basic Timer: wait 1 second
	timer1 := time.NewTimer(1 * time.Second)
	<-timer1.C // Blocks until channel receives timestamp
	fmt.Println("Timer 1 fired!")
 
	// 2. Canceling a timer before it fires
	timer2 := time.NewTimer(2 * time.Second)
	go func() {
		<-timer2.C
		fmt.Println("Timer 2 fired")
	}()
 
	stop2 := timer2.Stop()
	if stop2 {
		fmt.Println("Timer 2 canceled before firing!")
	}
}

2. Tickers: Periodic Loops (time.NewTicker)

A Ticker ticks repeatedly at regular intervals until explicitly stopped:

package main
 
import (
	"fmt"
	"time"
)
 
func main() {
	ticker := time.NewTicker(500 * time.Millisecond)
	done := make(chan bool)
 
	go func() {
		for {
			select {
			case <-done:
				return
			case t := <-ticker.C:
				fmt.Println("Tick at", t.Format("15:04:05.000"))
			}
		}
	}()
 
	time.Sleep(1600 * time.Millisecond)
	ticker.Stop() // Stop ticker to avoid leaking timer resources!
	done <- true
	fmt.Println("Ticker stopped cleanly.")
}

3. Rate Limiting: Basic & Bursty Token Buckets

Rate limiting is essential for protecting downstream APIs from traffic surges.

1. Basic Fixed-Rate Limiter (Using Ticker)

package main
 
import (
	"fmt"
	"time"
)
 
func main() {
	requests := make(chan int, 5)
	for i := 1; i <= 5; i++ {
		requests <- i
	}
	close(requests)
 
	// Rate limiter: 1 request every 200ms
	limiter := time.Tick(200 * time.Millisecond)
 
	for req := range requests {
		<-limiter // Blocks until ticker emits
		fmt.Println("Processed request", req, time.Now().Format("15:04:05.000"))
	}
}

2. Bursty Token Bucket (Allows Short Spikes)

In production APIs, you often want to allow a burst of up to 3 requests instantly, but sustain an overall limit of 1 request per 200ms:

package main
 
import (
	"fmt"
	"time"
)
 
func main() {
	// Burst capacity of 3 tokens
	burstyLimiter := make(chan time.Time, 3)
 
	// Fill the bucket to allow immediate burst of 3
	for i := 0; i < 3; i++ {
		burstyLimiter <- time.Now()
	}
 
	// Continuously refill the bucket at 200ms intervals
	go func() {
		for t := range time.Tick(200 * time.Millisecond) {
			burstyLimiter <- t
		}
	}()
 
	burstyRequests := make(chan int, 5)
	for i := 1; i <= 5; i++ {
		burstyRequests <- i
	}
	close(burstyRequests)
 
	for req := range burstyRequests {
		<-burstyLimiter // First 3 succeed immediately; remaining 2 wait 200ms each!
		fmt.Println("Bursty request", req, time.Now().Format("15:04:05.000"))
	}
}

4. Stateful Goroutines (The Actor Pattern)

Instead of using sync.Mutex to protect shared state, Go lets you confine state inside a single dedicated goroutine that communicates via read/write request channels:

package main
 
import (
	"fmt"
	"math/rand"
	"sync/atomic"
	"time"
)
 
type readOp struct {
	key  int
	resp chan int // Private channel to receive response
}
 
type writeOp struct {
	key  int
	val  int
	resp chan bool
}
 
func main() {
	var readOps uint64
	var writeOps uint64
 
	reads := make(chan readOp)
	writes := make(chan writeOp)
 
	// Sole owner of the state! Zero mutexes needed!
	go func() {
		state := make(map[int]int)
		for {
			select {
			case read := <-reads:
				read.resp <- state[read.key]
			case write := <-writes:
				state[write.key] = write.val
				write.resp <- true
			}
		}
	}()
 
	// 10 concurrent readers
	for r := 0; r < 10; r++ {
		go func() {
			for {
				read := readOp{key: rand.Intn(5), resp: make(chan int)}
				reads <- read
				<-read.resp
				atomic.AddUint64(&readOps, 1)
				time.Sleep(time.Millisecond)
			}
		}()
	}
 
	time.Sleep(500 * time.Millisecond)
	fmt.Printf("Read ops executed safely: %d\n", atomic.LoadUint64(&readOps))
}

Across the Wiki