Go

Design a per-client rate limiter for an HTTP API. Implement a token bucket.

Question 468HardGo 1.22 to 1.25

Token bucket: capacity b tokens, refilled at r per second; each request consumes one; allows bursts up to b. Compute tokens lazily from elapsed time — no background goroutine per bucket. In production, use golang.org/x/time/rate (rate.NewLimiter(r, b), Allow/Wait(ctx)).

type bucket struct {
	tokens float64
	last   time.Time
}

type Limiter struct {
	mu      sync.Mutex
	rate    float64 // tokens per second
	burst   float64
	buckets map[string]*bucket
}

func NewLimiter(ctx context.Context, rate float64, burst int) *Limiter {
	l := &Limiter{rate: rate, burst: float64(burst), buckets: make(map[string]*bucket)}
	go l.evict(ctx, 5*time.Minute) // prevent unbounded map growth
	return l
}

func (l *Limiter) Allow(key string) bool {
	now := time.Now()
	l.mu.Lock()
	defer l.mu.Unlock()
	b, ok := l.buckets[key]
	if !ok {
		b = &bucket{tokens: l.burst, last: now}
		l.buckets[key] = b
	}
	b.tokens = min(l.burst, b.tokens+now.Sub(b.last).Seconds()*l.rate) // Go 1.21 min
	b.last = now
	if b.tokens < 1 {
		return false
	}
	b.tokens--
	return true
}

func (l *Limiter) evict(ctx context.Context, idle time.Duration) {
	t := time.NewTicker(idle)
	defer t.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case now := <-t.C:
			l.mu.Lock()
			for k, b := range l.buckets {
				if now.Sub(b.last) > idle {
					delete(l.buckets, k)
				}
			}
			l.mu.Unlock()
		}
	}
}

func (l *Limiter) Middleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ip, _, _ := net.SplitHostPort(r.RemoteAddr)
		if !l.Allow(ip) {
			w.Header().Set("Retry-After", "1")
			http.Error(w, "rate limited", http.StatusTooManyRequests)
			return
		}
		next.ServeHTTP(w, r)
	})
}

Discussion points: a single mutex becomes contention — shard the map by key hash; behind a proxy, key by authenticated user or a trusted X-Forwarded-For; across multiple replicas, move state to Redis (atomic Lua script / GCRA) or accept per-instance limits.

More on Standard Library, HTTP & Systems Design in Go

All 35 Standard Library, HTTP & Systems Design in Go questions