How do you implement rate limiting in Go? Compare time.Ticker with golang.org/x/time/rate.
Question 246HardGo 1.22 to 1.25
Ticker: the simplest option is a fixed interval with no bursts.
t := time.NewTicker(100 * time.Millisecond) // 10 rps
defer t.Stop()
for _, req := range reqs {
select {
case <-t.C:
go handle(req)
case <-ctx.Done():
return ctx.Err()
}
}
Token bucket (x/time/rate): the rate is r tokens per second and the bucket holds up to b tokens (the burst).
Wait(ctx)blocks until a token is available.Allow()takes a token if one is available and never blocks. It suits HTTP 429 responses.Reserve()tells you how long to delay.
lim := rate.NewLimiter(rate.Every(100*time.Millisecond), 5) // 10 rps, burst 5
if err := lim.Wait(ctx); err != nil {
return err
}
// per-client middleware
type Limiters struct {
mu sync.Mutex
m map[string]*rate.Limiter
}
func (l *Limiters) get(ip string) *rate.Limiter {
l.mu.Lock()
defer l.mu.Unlock()
lim, ok := l.m[ip]
if !ok {
lim = rate.NewLimiter(5, 10)
l.m[ip] = lim
}
return lim
}
func (l *Limiters) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !l.get(clientIP(r)).Allow() {
http.Error(w, "too many requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
Gotchas:
- A per-client map grows without bound. Evict idle entries with a last-seen timestamp.
- In-process limiters do not coordinate across replicas. A global limit needs Redis or a gateway.
- Rate limiting controls requests per second. Bounding concurrency is a different tool: a semaphore.
More on Concurrency Patterns & sync
- Q244Explain errgroup: WithContext, SetLimit, TryGo. What are its semantics and gotchas?
- Q245You need to process 10 million records from a file with bounded memory and bounded parallelism. How do you design it?
- Q247What problem does singleflight solve, and what are its gotchas?
- Q248Implement graceful shutdown for an HTTP server with background workers.
- Q249Which newer context features matter for concurrent code: WithCancelCause, AfterFunc, WithoutCancel?
- Q250Is time.After in a select loop a leak? What changed in Go 1.23 timers?