go 47 lines · 7 steps

Rate limiting HTTP handlers with a token bucket

An http.Handler wrapper uses a token-bucket limiter to reserve, delay, or reject requests before passing them downstream.

Explained by highlit
1package server
2 
3import (
4 "net/http"
5 "time"
6 
7 "golang.org/x/time/rate"
8)
9 
10type RateLimitedHandler struct {
11 next http.Handler
12 limiter *rate.Limiter
13}
14 
15func NewRateLimitedHandler(next http.Handler, rps float64, burst int) *RateLimitedHandler {
16 return &RateLimitedHandler{
17 next: next,
18 limiter: rate.NewLimiter(rate.Limit(rps), burst),
19 }
20}
21 
22func (h *RateLimitedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
23 res := h.limiter.Reserve()
24 if !res.OK() {
25 http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
26 return
27 }
28 
29 if delay := res.Delay(); delay > 0 {
30 if delay > 2*time.Second {
31 res.Cancel()
32 w.Header().Set("Retry-After", delay.Round(time.Second).String())
33 http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
34 return
35 }
36 
37 select {
38 case <-time.After(delay):
39 case <-r.Context().Done():
40 res.Cancel()
41 http.Error(w, "request cancelled", http.StatusRequestTimeout)
42 return
43 }
44 }
45 
46 h.next.ServeHTTP(w, r)
47}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Wrapping an http.Handler lets you inject cross-cutting policy like rate limiting without touching downstream logic.
  2. 2Reserve gives you the wait time up front so you can choose to sleep, reject, or cancel instead of blocking blindly.
  3. 3Always honor request context cancellation and return reserved tokens when you bail out so capacity isn't wasted.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Rate limiting HTTP handlers with a token bucket — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code