Token Bucket Rate-Limiting Middleware for a Go net/http API

Shared by codesnips Jul 2026
4 tabs
package ratelimit

import (
	"sync"
	"time"
)

type Bucket struct {
	mu           sync.Mutex
	capacity     float64
	tokens       float64
	refillPerSec float64
	last         time.Time
}

func NewBucket(capacity, refillPerSec float64) *Bucket {
	return &Bucket{
		capacity:     capacity,
		tokens:       capacity,
		refillPerSec: refillPerSec,
		last:         time.Now(),
	}
}

func (b *Bucket) refill(now time.Time) {
	elapsed := now.Sub(b.last).Seconds()
	if elapsed <= 0 {
		return
	}
	b.tokens += elapsed * b.refillPerSec
	if b.tokens > b.capacity {
		b.tokens = b.capacity
	}
	b.last = now
}

func (b *Bucket) Allow() (bool, float64) {
	b.mu.Lock()
	defer b.mu.Unlock()
	b.refill(time.Now())
	if b.tokens < 1 {
		return false, b.tokens
	}
	b.tokens--
	return true, b.tokens
}

func (b *Bucket) isFull() bool {
	b.mu.Lock()
	defer b.mu.Unlock()
	return b.tokens >= b.capacity
}
4 files · go Explain with highlit

This snippet implements per-client rate limiting for a Go HTTP API using the classic token bucket algorithm, wrapped as standard net/http middleware. A token bucket models a fixed-capacity bucket that refills at a steady rate: every request tries to remove one token, and when the bucket is empty the request is rejected. Because the bucket has capacity, it tolerates short bursts while still enforcing an average rate over time, which is usually what an API wants — smooth throughput without punishing the occasional spike.

The tokenbucket.go tab holds the pure algorithm, deliberately free of any HTTP concerns so it can be unit tested in isolation. Bucket stores its state as a floating-point tokens count rather than integers, which lets the refill be computed lazily: instead of running a background goroutine that ticks tokens in, refill looks at the elapsed time since the last call and adds elapsed * refillPerSec tokens, clamped to capacity. This lazy, on-demand refill is the key trade-off — it avoids one timer per client and scales to many buckets cheaply, at the cost of doing a little arithmetic on every Allow call. The mutex makes a single bucket safe under concurrent requests, and Allow returns false without blocking when fewer than one token remains.

The limiter.go tab manages a map of buckets keyed by client identity so that one noisy client cannot exhaust another's budget. Limiter guards the map with its own sync.Mutex and lazily creates a bucket the first time a key is seen in getBucket. A real deployment would also evict idle buckets to bound memory; the cleanup loop shows the shape of that, sweeping entries whose bucket has been full and untouched for a while. Keying by IP is simple but imperfect behind proxies, so the middleware allows the key function to be injected.

The middleware.go tab bridges the limiter to net/http. RateLimit is a higher-order function returning a func(http.Handler) http.Handler, the idiomatic middleware signature that composes cleanly with routers like chi or the standard ServeMux. It derives a key via keyFunc, calls Allow, and on rejection writes standards-friendly headers: Retry-After tells clients when to try again and X-RateLimit-Remaining reports the budget, then returns 429 Too Many Requests. When a request is allowed it simply calls next.ServeHTTP, so allowed traffic pays almost no overhead.

A subtle pitfall worth noting is clock choice: the code uses time.Now() for elapsed time, which is fine in practice but sensitive to wall-clock jumps; a monotonic source avoids that. Another is that this limiter is per-process — behind a load balancer each instance keeps its own buckets, so the effective global rate is multiplied by the instance count. For a single service or a sidecar this in-memory approach is fast and dependency-free; for a fleet a shared store like Redis with the same token-bucket math is the natural next step.

Share this code

Here's the card — post it anywhere.

Token Bucket Rate-Limiting Middleware for a Go net/http API — share card
Link copied