Go

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

All 38 Concurrency Patterns & sync questions