Day 21 — time Package
Learning Objectives
- Build a sliding-window rate limiter — the algorithm used by AWS API Gateway and Stripe
- Use
time.NewTickervstime.Tick(the latter leaks) - Understand monotonic clock vs wall clock in Go
Key Concepts
// time.After — creates a timer channel, fires once
// WARNING: leaks if you don't receive from it
select {
case <-time.After(5 * time.Second):
return errors.New("timeout")
case result := <-ch:
return result
}
// time.NewTicker — reusable ticker, MUST Stop() to prevent leak
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for range ticker.C {
// runs every second
}
// time.Tick — convenience wrapper, LEAKS (can't stop)
// Only use for package-level tickers that run forever
for range time.Tick(time.Second) { } // OK at top-level, bad in functions
// Monotonic clock: time.Since, time.Until use it automatically
// Wall clock: time.Now().Format(...), time.Parse use it
start := time.Now()
// ... work ...
elapsed := time.Since(start) // uses monotonic — correct even across DST changes
Exercise: Sliding-Window Rate Limiter
package main
import (
"fmt"
"sync"
"time"
)
// SlidingWindowLimiter implements the sliding window counter algorithm.
// More accurate than fixed-window at boundary conditions.
type SlidingWindowLimiter struct {
mu sync.Mutex
limit int // max requests per window
window time.Duration // window size
buckets map[string]*windowState
cleanupTick *time.Ticker
}
type windowState struct {
count int
windowEnd time.Time
prevCount int // count from previous window (for sliding approximation)
prevEnd time.Time
}
func NewSlidingWindowLimiter(limit int, window time.Duration) *SlidingWindowLimiter {
l := &SlidingWindowLimiter{
limit: limit,
window: window,
buckets: make(map[string]*windowState),
cleanupTick: time.NewTicker(window * 2),
}
go l.cleanup()
return l
}
func (l *SlidingWindowLimiter) Allow(key string) bool {
l.mu.Lock()
defer l.mu.Unlock()
now := time.Now()
state, ok := l.buckets[key]
if !ok {
state = &windowState{windowEnd: now.Add(l.window)}
l.buckets[key] = state
}
// Rotate window if expired
if now.After(state.windowEnd) {
state.prevCount = state.count
state.prevEnd = state.windowEnd
state.count = 0
state.windowEnd = now.Add(l.window)
}
// Sliding window: weighted sum of current + fraction of previous window
elapsed := now.Sub(state.prevEnd)
prevWeight := 1.0 - float64(elapsed)/float64(l.window)
if prevWeight < 0 { prevWeight = 0 }
estimated := float64(state.prevCount)*prevWeight + float64(state.count)
if estimated >= float64(l.limit) {
return false
}
state.count++
return true
}
func (l *SlidingWindowLimiter) cleanup() {
for range l.cleanupTick.C {
l.mu.Lock()
cutoff := time.Now().Add(-l.window * 2)
for key, state := range l.buckets {
if state.windowEnd.Before(cutoff) {
delete(l.buckets, key)
}
}
l.mu.Unlock()
}
}
func (l *SlidingWindowLimiter) Stop() { l.cleanupTick.Stop() }
func main() {
limiter := NewSlidingWindowLimiter(10, time.Second)
defer limiter.Stop()
clientIP := "192.168.1.1"
allowed, blocked := 0, 0
for i := 0; i < 25; i++ {
if limiter.Allow(clientIP) {
allowed++
} else {
blocked++
}
}
fmt.Printf("Burst test: %d allowed, %d blocked (limit: 10)\n", allowed, blocked)
// Wait for window to reset
time.Sleep(1100 * time.Millisecond)
allowed, blocked = 0, 0
for i := 0; i < 5; i++ {
if limiter.Allow(clientIP) {
allowed++
} else {
blocked++
}
}
fmt.Printf("After reset: %d allowed, %d blocked\n", allowed, blocked)
}
Real-World Context
This rate limiter is the foundation of Day 76 (per-API-key token bucket) and Day 93 (per-IP DDoS mitigation). The sliding window algorithm avoids the "double burst" problem of fixed windows — an attacker can't send 100% of limit at the end of window 1 + 100% at the start of window 2. AWS API Gateway uses exactly this approach.