{
  "id": "go",
  "title": "Token bucket rate limiter with Redis and Go",
  "url": "https://redis.io/docs/latest/develop/use-cases/rate-limiter/go/",
  "summary": "Implement a token bucket rate limiter using Redis and Lua scripts in Go",
  "tags": [
    "docs",
    "develop",
    "stack",
    "oss",
    "rs",
    "rc"
  ],
  "last_updated": "2026-04-16T13:29:55-07:00",
  "children": [],
  "page_type": "content",
  "content_hash": "05f2cbaed0423a92ee81807925308fe6da8c84be07dabbfed8c7d82daccbb559",
  "sections": [
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "This guide shows you how to implement a distributed token bucket rate limiter using Redis and Lua scripts in Go with the [`go-redis`](https://redis.io/docs/latest/develop/clients/go) client library."
    },
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "Rate limiting is a critical technique for controlling the rate at which operations are performed. Common use cases include:\n\n* Limiting API requests per user or IP address\n* Preventing abuse and protecting against denial-of-service attacks\n* Ensuring fair resource allocation across multiple clients\n* Throttling background jobs or batch operations\n\nThe **token bucket algorithm** is a popular rate limiting approach that allows bursts of traffic while maintaining an average rate limit over time. This guide covers the Go implementation using the [`go-redis`](https://redis.io/docs/latest/develop/clients/go) client library, taking advantage of Go's `context.Context` for cancellation and timeouts, explicit error handling, and goroutine safety."
    },
    {
      "id": "how-it-works",
      "title": "How it works",
      "role": "content",
      "text": "The token bucket algorithm works like a bucket that holds tokens:\n\n1. **Initialization**: The bucket starts with a maximum capacity of tokens\n2. **Refill**: Tokens are added to the bucket at a constant rate (for example, 1 token per second)\n3. **Consumption**: Each request consumes one token from the bucket\n4. **Decision**: If tokens are available, the request is allowed; otherwise, it's denied\n5. **Capacity limit**: The bucket never exceeds its maximum capacity\n\nThis approach allows for burst traffic (using accumulated tokens) while enforcing an average rate limit over time."
    },
    {
      "id": "why-use-redis",
      "title": "Why use Redis?",
      "role": "content",
      "text": "Redis is ideal for distributed rate limiting because:\n\n* **Atomic operations**: Lua scripts execute atomically, preventing race conditions\n* **Shared state**: Multiple application servers can share the same rate limit counters\n* **High performance**: In-memory operations provide microsecond latency\n* **Automatic expiration**: Keys can be set to expire automatically (though not used in this implementation)"
    },
    {
      "id": "the-lua-script",
      "title": "The Lua script",
      "role": "content",
      "text": "The core of this implementation is a Lua script that runs atomically on the Redis server. This ensures that checking and updating the token bucket happens in a single operation, preventing race conditions in distributed environments.\n\nHere's how the script works:\n\n[code example]"
    },
    {
      "id": "script-breakdown",
      "title": "Script breakdown",
      "role": "content",
      "text": "1. **State retrieval**: Uses [`HMGET`](https://redis.io/docs/latest/commands/hmget) to fetch the current token count and last refill time from a hash\n2. **Initialization**: On first use, sets tokens to full capacity\n3. **Token refill calculation**: Computes how many tokens should be added based on elapsed time\n4. **Capacity enforcement**: Uses `math.min()` to ensure tokens never exceed capacity\n5. **Token consumption**: Decrements the token count if available\n6. **State update**: Uses [`HMSET`](https://redis.io/docs/latest/commands/hmset) to save the new state\n7. **Return value**: Returns both the decision (allowed/denied) and remaining tokens"
    },
    {
      "id": "why-atomicity-matters",
      "title": "Why atomicity matters",
      "role": "content",
      "text": "Without atomic execution, race conditions could occur:\n\n* **Double spending**: Two requests could read the same token count and both succeed when only one should\n* **Lost updates**: Concurrent updates could overwrite each other's changes\n* **Inconsistent state**: Token count and refill time could become desynchronized\n\nUsing [`EVAL`](https://redis.io/docs/latest/commands/eval) or [`EVALSHA`](https://redis.io/docs/latest/commands/evalsha) ensures the entire operation executes atomically, making it safe for distributed systems."
    },
    {
      "id": "installation",
      "title": "Installation",
      "role": "setup",
      "text": "Install the `go-redis` package:\n\n[code example]"
    },
    {
      "id": "using-the-go-package",
      "title": "Using the Go package",
      "role": "content",
      "text": "The `TokenBucket` struct provides a concurrency-safe interface for rate limiting\n([source](token_bucket.go)):\n\n[code example]\n\nGo's `context.Context` is passed to every call, allowing you to set deadlines and handle cancellation. The `Allow` method returns an explicit `error` value following Go conventions."
    },
    {
      "id": "configuration-parameters",
      "title": "Configuration parameters",
      "role": "configuration",
      "text": "* **Capacity**: Maximum number of tokens in the bucket (controls burst size)\n* **RefillRate**: Number of tokens added per refill interval\n* **RefillInterval**: Time in seconds between refills\n\nFor example:\n* `Capacity: 10, RefillRate: 1, RefillInterval: 1.0` allows 10 requests per second with bursts up to 10\n* `Capacity: 100, RefillRate: 10, RefillInterval: 1.0` allows 10 requests per second with bursts up to 100\n* `Capacity: 60, RefillRate: 1, RefillInterval: 60.0` allows 1 request per minute with bursts up to 60"
    },
    {
      "id": "rate-limit-keys",
      "title": "Rate limit keys",
      "role": "content",
      "text": "The `key` parameter identifies what you're rate limiting. Common patterns:\n\n* **Per user**: `user:{userID}` - Limit each user independently\n* **Per IP address**: `ip:{ipAddress}` - Limit by client IP\n* **Per API endpoint**: `api:{endpoint}:{userID}` - Different limits per endpoint\n* **Global**: `global:api` - Single limit shared across all requests"
    },
    {
      "id": "script-caching-with-evalsha",
      "title": "Script caching with EVALSHA",
      "role": "content",
      "text": "The Go implementation uses [`EVALSHA`](https://redis.io/docs/latest/commands/evalsha) for optimal performance. On first use, the Lua script is loaded into Redis with `SCRIPT LOAD`, and subsequent calls use the cached SHA1 hash. If the script is evicted from the cache, the module automatically falls back to [`EVAL`](https://redis.io/docs/latest/commands/eval) and reloads the script. The script loading is protected with a `sync.Once` to ensure thread safety across goroutines.\n\n[code example]"
    },
    {
      "id": "goroutine-safety",
      "title": "Goroutine safety",
      "role": "content",
      "text": "The `TokenBucket` struct is safe for concurrent use from multiple goroutines. You can share a single limiter instance across your application:\n\n[code example]"
    },
    {
      "id": "running-the-demo",
      "title": "Running the demo",
      "role": "content",
      "text": "A demonstration HTTP server is included to show the rate limiter in action\n([source](demo_server.go)):\n\nTo run the demo, create a `main.go` file that calls the exported `RunDemoServer()` function:\n\n[code example]\n\nThen build and run:\n\n[code example]\n\nThe demo provides an interactive web interface where you can:\n\n* Submit requests and see them allowed or denied in real-time\n* View the current token count\n* Adjust rate limit parameters dynamically\n* Test different rate limiting scenarios\n\nThe demo assumes Redis is running on `localhost:6379` but you can specify a different host and port using the `--redis-host` and `--redis-port` command-line arguments. Visit `http://localhost:8080` in your browser to try it out."
    },
    {
      "id": "response-headers",
      "title": "Response headers",
      "role": "returns",
      "text": "It's common to include rate limit information in HTTP response headers:\n\n[code example]"
    },
    {
      "id": "customization",
      "title": "Customization",
      "role": "content",
      "text": ""
    },
    {
      "id": "using-as-http-middleware",
      "title": "Using as HTTP middleware",
      "role": "content",
      "text": "You can wrap the rate limiter as HTTP middleware for use with the standard library or any compatible router:\n\n[code example]"
    },
    {
      "id": "error-handling",
      "title": "Error handling",
      "role": "errors",
      "text": "The `Allow` method returns an error if the Redis connection is lost. Decide whether to fail open (allow requests) or fail closed (deny requests) based on your requirements:\n\n[code example]"
    },
    {
      "id": "using-with-context-timeouts",
      "title": "Using with context timeouts",
      "role": "content",
      "text": "Go's `context.Context` lets you set deadlines for rate limit checks, which is useful when you don't want a slow Redis connection to block request processing:\n\n[code example]"
    },
    {
      "id": "learn-more",
      "title": "Learn more",
      "role": "related",
      "text": "* [EVAL command](https://redis.io/docs/latest/commands/eval) - Execute Lua scripts\n* [EVALSHA command](https://redis.io/docs/latest/commands/evalsha) - Execute cached Lua scripts\n* [Lua scripting](https://redis.io/docs/latest/develop/programmability/eval-intro) - Introduction to Redis Lua scripting\n* [HMGET command](https://redis.io/docs/latest/commands/hmget) - Get multiple hash fields\n* [HMSET command](https://redis.io/docs/latest/commands/hmset) - Set multiple hash fields\n* [Go client](https://redis.io/docs/latest/develop/clients/go) - Redis Go client documentation"
    }
  ],
  "examples": [
    {
      "id": "the-lua-script-ex0",
      "language": "lua",
      "code": "local key = KEYS[1]\nlocal capacity = tonumber(ARGV[1])\nlocal refill_rate = tonumber(ARGV[2])\nlocal refill_interval = tonumber(ARGV[3])\nlocal now = tonumber(ARGV[4])\n\n-- Get current state or initialize\nlocal bucket = redis.call('HMGET', key, 'tokens', 'last_refill')\nlocal tokens = tonumber(bucket[1])\nlocal last_refill = tonumber(bucket[2])\n\n-- Initialize if this is the first request\nif tokens == nil then\n    tokens = capacity\n    last_refill = now\nend\n\n-- Calculate token refill\nlocal time_passed = now - last_refill\nlocal refills = math.floor(time_passed / refill_interval)\n\nif refills > 0 then\n    tokens = math.min(capacity, tokens + (refills * refill_rate))\n    last_refill = last_refill + (refills * refill_interval)\nend\n\n-- Try to consume a token\nlocal allowed = 0\nif tokens >= 1 then\n    tokens = tokens - 1\n    allowed = 1\nend\n\n-- Update state\nredis.call('HMSET', key, 'tokens', tokens, 'last_refill', last_refill)\n\n-- Return result: allowed (1 or 0) and remaining tokens\nreturn {allowed, tokens}",
      "section_id": "the-lua-script"
    },
    {
      "id": "installation-ex0",
      "language": "bash",
      "code": "go get github.com/redis/go-redis/v9",
      "section_id": "installation"
    },
    {
      "id": "using-the-go-package-ex0",
      "language": "go",
      "code": "package main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"log\"\n    \"time\"\n\n    \"github.com/redis/go-redis/v9\"\n)\n\nfunc main() {\n    ctx := context.Background()\n\n    // Create a Redis connection\n    rdb := redis.NewClient(&redis.Options{\n        Addr: \"localhost:6379\",\n    })\n\n    // Create a rate limiter: 10 requests per second\n    limiter := NewTokenBucket(TokenBucketConfig{\n        Client:         rdb,\n        Capacity:       10,           // Maximum burst size\n        RefillRate:     1,            // Add 1 token per interval\n        RefillInterval: time.Second,  // Every 1 second\n    })\n\n    // Check if a request should be allowed\n    allowed, remaining, err := limiter.Allow(ctx, \"user:123\")\n    if err != nil {\n        log.Fatalf(\"rate limiter error: %v\", err)\n    }\n\n    if allowed {\n        fmt.Printf(\"Request allowed. %.0f tokens remaining.\\n\", remaining)\n        // Process the request\n    } else {\n        fmt.Println(\"Request denied. Rate limit exceeded.\")\n        // Return 429 Too Many Requests\n    }\n}",
      "section_id": "using-the-go-package"
    },
    {
      "id": "script-caching-with-evalsha-ex0",
      "language": "go",
      "code": "// The package handles script caching automatically.\n// First call loads the script, subsequent calls use EVALSHA.\nallowed1, remaining1, _ := limiter.Allow(ctx, \"user:123\") // Uses EVAL + caches\nallowed2, remaining2, _ := limiter.Allow(ctx, \"user:123\") // Uses EVALSHA (faster)",
      "section_id": "script-caching-with-evalsha"
    },
    {
      "id": "goroutine-safety-ex0",
      "language": "go",
      "code": "limiter := NewTokenBucket(TokenBucketConfig{\n    Client:         rdb,\n    Capacity:       10,\n    RefillRate:     1,\n    RefillInterval: time.Second,\n})\n\n// Safe to call from multiple goroutines\nvar wg sync.WaitGroup\nfor i := 0; i < 20; i++ {\n    wg.Add(1)\n    go func(id int) {\n        defer wg.Done()\n        allowed, remaining, err := limiter.Allow(ctx, \"shared:resource\")\n        if err != nil {\n            log.Printf(\"goroutine %d: error: %v\", id, err)\n            return\n        }\n        log.Printf(\"goroutine %d: allowed=%v remaining=%.0f\", id, allowed, remaining)\n    }(i)\n}\nwg.Wait()",
      "section_id": "goroutine-safety"
    },
    {
      "id": "running-the-demo-ex0",
      "language": "go",
      "code": "package main\n\nimport \"ratelimiter\"\n\nfunc main() { ratelimiter.RunDemoServer() }",
      "section_id": "running-the-demo"
    },
    {
      "id": "running-the-demo-ex1",
      "language": "bash",
      "code": "# Install dependencies\ngo get github.com/redis/go-redis/v9\n\n# Build and run the demo server\ngo build -o demo ./...\n./demo",
      "section_id": "running-the-demo"
    },
    {
      "id": "response-headers-ex0",
      "language": "go",
      "code": "// Store config values for use in response headers\ncapacity := 10\nrefillInterval := time.Second\n\nlimiter := NewTokenBucket(TokenBucketConfig{\n    Client:         rdb,\n    Capacity:       capacity,\n    RefillRate:     1,\n    RefillInterval: refillInterval,\n})\n\nallowed, remaining, err := limiter.Allow(ctx, fmt.Sprintf(\"user:%s\", userID))\nif err != nil {\n    http.Error(w, \"Internal Server Error\", http.StatusInternalServerError)\n    return\n}\n\n// Add standard rate limit headers\nw.Header().Set(\"X-RateLimit-Limit\", strconv.Itoa(capacity))\nw.Header().Set(\"X-RateLimit-Remaining\", strconv.FormatFloat(remaining, 'f', 0, 64))\nw.Header().Set(\"X-RateLimit-Reset\", strconv.FormatInt(\n    time.Now().Add(refillInterval).Unix(), 10))\n\nif !allowed {\n    w.Header().Set(\"Retry-After\", strconv.FormatFloat(refillInterval.Seconds(), 'f', 0, 64))\n    http.Error(w, \"Too Many Requests\", http.StatusTooManyRequests)\n    return\n}",
      "section_id": "response-headers"
    },
    {
      "id": "using-as-http-middleware-ex0",
      "language": "go",
      "code": "func RateLimitMiddleware(limiter *TokenBucket, keyFn func(*http.Request) string) func(http.Handler) http.Handler {\n    return func(next http.Handler) http.Handler {\n        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n            key := keyFn(r)\n            allowed, remaining, err := limiter.Allow(r.Context(), key)\n            if err != nil {\n                http.Error(w, \"Internal Server Error\", http.StatusInternalServerError)\n                return\n            }\n\n            w.Header().Set(\"X-RateLimit-Remaining\", strconv.FormatFloat(remaining, 'f', 0, 64))\n\n            if !allowed {\n                http.Error(w, \"Rate limit exceeded\", http.StatusTooManyRequests)\n                return\n            }\n            next.ServeHTTP(w, r)\n        })\n    }\n}\n\n// Apply per-IP rate limiting\nmux := http.NewServeMux()\nhandler := RateLimitMiddleware(limiter, func(r *http.Request) string {\n    return fmt.Sprintf(\"ip:%s\", r.RemoteAddr)\n})(mux)",
      "section_id": "using-as-http-middleware"
    },
    {
      "id": "error-handling-ex0",
      "language": "go",
      "code": "allowed, remaining, err := limiter.Allow(ctx, \"user:123\")\nif err != nil {\n    log.Printf(\"rate limiter error: %v\", err)\n    // Fail open: allow the request when Redis is unavailable\n    allowed = true\n    // Or fail closed: deny the request\n    // allowed = false\n}",
      "section_id": "error-handling"
    },
    {
      "id": "using-with-context-timeouts-ex0",
      "language": "go",
      "code": "// Set a 100ms timeout for the rate limit check\nctx, cancel := context.WithTimeout(r.Context(), 100*time.Millisecond)\ndefer cancel()\n\nallowed, remaining, err := limiter.Allow(ctx, \"user:123\")\nif err != nil {\n    // Context deadline exceeded or Redis error\n    log.Printf(\"rate limit check timed out: %v\", err)\n    allowed = true // fail open\n}",
      "section_id": "using-with-context-timeouts"
    }
  ]
}
