Master Go Rate Limiting: Token Bucket, IP-Based, and Redis Distributed Strategies

This article explains how to protect Go services from overload by implementing HTTP request rate limiting using the standard token‑bucket limiter, per‑IP throttling with a map and mutex, and a distributed approach with Redis and Lua scripts, complete with practical code examples and middleware integration.

php Courses
php Courses
php Courses
Master Go Rate Limiting: Token Bucket, IP-Based, and Redis Distributed Strategies

Implementing HTTP request rate limiting in Go helps prevent services from being overwhelmed and protects backend resources. The article covers three practical approaches: the built‑in token‑bucket limiter, per‑IP throttling, and a distributed solution using Redis.

Basic Rate Limiting with golang.org/x/time/rate

The rate.Limiter type from golang.org/x/time/rate provides a token‑bucket algorithm suitable for single‑instance scenarios. Each request must acquire a token; if none are available, the request is rejected or delayed.

package main

import (
    "golang.org/x/time/rate"
    "net/http"
    "time"
)

var limiter = rate.NewLimiter(10, 50) // 10 tokens per second, burst up to 50

func limit(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if !limiter.Allow() {
            http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
            return
        }
        next(w, r)
    }
}

func handler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello, Rate Limited World!"))
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", limit(handler))
    http.ListenAndServe(":8080", mux)
}

This example limits the server to a maximum of 10 requests per second, allowing bursts of up to 50. Excess requests receive a 429 status code.

IP‑Based Independent Limiting

In real‑world deployments, different clients (identified by IP) often need separate limits. A thread‑safe map (or a concurrent map library) can store a rate.Limiter for each IP.

type IpLimiter struct {
    visitors map[string]*rate.Limiter
    mu       *sync.RWMutex
    limit    rate.Limit
    burst    int
}

func NewIpLimiter(r rate.Limit, b int) *IpLimiter {
    return &IpLimiter{
        visitors: make(map[string]*rate.Limiter),
        mu:       &sync.RWMutex{},
        limit:    r,
        burst:    b,
    }
}

func (i *IpLimiter) getLimiter(ip string) *rate.Limiter {
    i.mu.RLock()
    limiter, exists := i.visitors[ip]
    i.mu.RUnlock()
    if !exists {
        i.mu.Lock()
        if _, found := i.visitors[ip]; !found {
            i.visitors[ip] = rate.NewLimiter(i.limit, i.burst)
        }
        limiter = i.visitors[ip]
        i.mu.Unlock()
    }
    return limiter
}

Middleware usage:

var ipLimiter = NewIpLimiter(1, 5) // 1 request per second, burst up to 5

func ipLimit(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ip := r.RemoteAddr // In production, parse X‑Forwarded‑For or X‑Real‑IP
        if !ipLimiter.getLimiter(ip).Allow() {
            http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
            return
        }
        next(w, r)
    }
}

Distributed Rate Limiting with Redis

When multiple service instances run, in‑memory limits cannot be shared. Redis can store counters and enforce limits atomically using Lua scripts. Common algorithms include the leaky‑bucket and fixed‑window approaches.

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])

local current = redis.call("INCR", key)
if current == 1 then
    redis.call("EXPIRE", key, window)
end
if current > limit then
    return 0
end
return 1

Go integration using redigo:

import "github.com/gomodule/redigo/redis"

func allowRequest(ip string, conn redis.Conn) (bool, error) {
    script := redis.NewScript(1, `
local current = redis.call("INCR", KEYS[1])
if current == 1 then
    redis.call("EXPIRE", KEYS[1], ARGV[2])
end
if current > tonumber(ARGV[1]) then
    return 0
end
return 1
    `)
    result, err := redis.Int(script.Do(conn, ip, "10", "60")) // max 10 requests per minute
    if err != nil {
        return false, err
    }
    return result == 1, nil
}

Middleware and Graceful Error Handling

Encapsulating rate‑limit logic in middleware makes it reusable. When a request exceeds the limit, return a 429 status and include a Retry-After header to guide clients.

if !limiter.Allow() {
    w.Header().Set("Retry-After", "1")
    http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
    return
}

Key takeaways: choose a simple token‑bucket limiter for single‑instance services, switch to per‑IP maps for finer granularity, and adopt Redis + Lua for distributed environments. Set sensible thresholds, monitor traffic, and log violations for alerting.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

middlewareredisGoHTTPrate limiting
php Courses
Written by

php Courses

php中文网's platform for the latest courses and technical articles, helping PHP learners advance quickly.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.