Master Go Rate Limiting: Sliding Window, Token Bucket, and Redis Techniques
This article presents four practical Go rate‑limiting implementations—a sliding‑window algorithm, a token‑bucket approach, the built‑in golang.org/x/time/rate package, and a Redis‑backed distributed limiter—complete with code samples, usage guidance, and recommendations for different deployment scenarios.
Introduction
Rate limiting is essential for protecting services from overload and abuse. In Go, several common strategies can be used, each with its own trade‑offs. The following sections describe four practical implementations and show how to choose the right one for your environment.
Scheme 1: Sliding Window (Recommended)
This method keeps a timestamp list for each user and removes entries that fall outside the configured time window. It provides precise request counting within the window.
package main
import (
"sync"
"time"
)
type RateLimiter struct {
mu sync.Mutex
requests map[string][]time.Time
limit int
window time.Duration
}
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
return &RateLimiter{
requests: make(map[string][]time.Time),
limit: limit,
window: window,
}
}
func (rl *RateLimiter) Allow(userID string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
windowStart := now.Add(-rl.window)
// Clean up expired request records
validRequests := make([]time.Time, 0)
for _, t := range rl.requests[userID] {
if t.After(windowStart) {
validRequests = append(validRequests, t)
}
}
// Check if the limit is exceeded
if len(validRequests) >= rl.limit {
return false
}
// Add the new request
validRequests = append(validRequests, now)
rl.requests[userID] = validRequests
// Optional periodic cleanup to avoid memory leaks
if len(validRequests) == 1 {
go rl.cleanup(userID, now)
}
return true
}
func (rl *RateLimiter) cleanup(userID string, baseTime time.Time) {
time.Sleep(rl.window)
rl.mu.Lock()
defer rl.mu.Unlock()
if requests, exists := rl.requests[userID]; exists {
windowStart := baseTime.Add(-rl.window)
validRequests := make([]time.Time, 0)
for _, t := range requests {
if t.After(windowStart) {
validRequests = append(validRequests, t)
}
}
if len(validRequests) == 0 {
delete(rl.requests, userID)
} else {
rl.requests[userID] = validRequests
}
}
}Scheme 2: Token Bucket Algorithm
The token bucket allows bursts up to a configured capacity while maintaining a steady refill rate, making it suitable for scenarios that require smooth traffic shaping.
package main
import (
"sync"
"time"
)
type TokenBucket struct {
mu sync.Mutex
tokens float64
maxTokens float64
refillRate float64 // tokens per second
lastRefill time.Time
}
func NewTokenBucket(maxTokens, refillRate float64) *TokenBucket {
return &TokenBucket{tokens: maxTokens, maxTokens: maxTokens, refillRate: refillRate, lastRefill: time.Now()}
}
func (tb *TokenBucket) refill() {
now := time.Now()
duration := now.Sub(tb.lastRefill)
tokensToAdd := tb.refillRate * duration.Seconds()
tb.tokens += tokensToAdd
if tb.tokens > tb.maxTokens {
tb.tokens = tb.maxTokens
}
tb.lastRefill = now
}
func (tb *TokenBucket) Take(tokens float64) bool {
tb.mu.Lock()
defer tb.mu.Unlock()
tb.refill()
if tb.tokens >= tokens {
tb.tokens -= tokens
return true
}
return false
}
type UserRateLimiter struct {
mu sync.RWMutex
users map[string]*TokenBucket
limit int
window time.Duration
}
func NewUserRateLimiter(limit int, window time.Duration) *UserRateLimiter {
refillRate := float64(limit) / window.Seconds()
return &UserRateLimiter{users: make(map[string]*TokenBucket), limit: limit, window: window}
}
func (rl *UserRateLimiter) Allow(userID string) bool {
rl.mu.RLock()
bucket, exists := rl.users[userID]
rl.mu.RUnlock()
if !exists {
rl.mu.Lock()
bucket = NewTokenBucket(float64(rl.limit), float64(rl.limit)/rl.window.Seconds())
rl.users[userID] = bucket
rl.mu.Unlock()
}
return bucket.Take(1)
}Scheme 3: Using golang.org/x/time/rate Package
The official Go rate package provides a ready‑made token‑bucket implementation with thread‑safe primitives.
package main
import (
"sync"
"time"
"golang.org/x/time/rate"
)
type RateLimiter struct {
mu sync.RWMutex
users map[string]*rate.Limiter
limit rate.Limit
burst int
}
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
r := rate.Every(window / time.Duration(limit))
return &RateLimiter{users: make(map[string]*rate.Limiter), limit: r, burst: limit}
}
func (rl *RateLimiter) GetLimiter(userID string) *rate.Limiter {
rl.mu.RLock()
limiter, exists := rl.users[userID]
rl.mu.RUnlock()
if !exists {
rl.mu.Lock()
limiter = rate.NewLimiter(rl.limit, rl.burst)
rl.users[userID] = limiter
rl.mu.Unlock()
}
return limiter
}
func (rl *RateLimiter) Allow(userID string) bool {
return rl.GetLimiter(userID).Allow()
}
func (rl *RateLimiter) CleanupInactiveUsers(timeout time.Duration) {
ticker := time.NewTicker(time.Hour)
go func() {
for range ticker.C {
rl.mu.Lock()
for userID, limiter := range rl.users {
// Placeholder for real inactivity check
_ = limiter
delete(rl.users, userID)
}
rl.mu.Unlock()
}
}()
}Scheme 4: Redis‑Based Distributed Limiter
For multi‑instance deployments, a Redis script can enforce limits atomically across processes.
package main
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
type RedisRateLimiter struct {
client *redis.Client
limit int
window time.Duration
}
func NewRedisRateLimiter(client *redis.Client, limit int, window time.Duration) *RedisRateLimiter {
return &RedisRateLimiter{client: client, limit: limit, window: window}
}
func (rl *RedisRateLimiter) Allow(ctx context.Context, userID string) (bool, error) {
now := time.Now().UnixNano()
windowSize := rl.window.Nanoseconds()
key := fmt.Sprintf("rate_limit:%s", userID)
luaScript := `
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local current = redis.call('ZCARD', key)
if current < limit then
redis.call('ZADD', key, now, now)
redis.call('EXPIRE', key, math.ceil(window / 1000000000))
return 1
end
return 0
`
result, err := rl.client.Eval(ctx, luaScript, []string{key}, now, windowSize, rl.limit).Result()
if err != nil {
return false, err
}
return result.(int64) == 1, nil
}Usage Example
The following snippet shows how to plug a limiter into an HTTP handler, using the sliding‑window implementation as an example.
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
limiter := NewRateLimiter(1000, time.Minute)
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("X-User-ID")
if userID == "" {
userID = r.RemoteAddr // fallback identifier
}
if !limiter.Allow(userID) {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
fmt.Fprint(w, "Request successful")
})
http.ListenAndServe(":8080", nil)
}Recommendations
Single‑node applications : Use Scheme 1 (sliding window) or Scheme 3 (golang.org/x/time/rate) for simplicity and efficiency.
Distributed systems : Adopt Scheme 4 with Redis to keep limits consistent across instances.
Need smooth throttling with bursts : Choose Scheme 2 (token bucket) to allow occasional spikes.
Production environments : Combine the limiter with monitoring and alerting to track rejected requests.
Choosing the right approach depends on factors such as distribution requirements, memory constraints, and precision needs.
Source: Go language community article (originally published on WeChat).
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Go Development Architecture Practice
Daily sharing of Golang-related technical articles, practical resources, language news, tutorials, real-world projects, and more. Looking forward to growing together. Let's go!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
