Implementing a Go and Redis-powered Sliding Window Rate Limiter
Grace Collins
Solutions Engineer · Leapcell

Introduction
In the ever-evolving landscape of distributed systems and microservices, managing API traffic effectively is paramount for maintaining system stability, preventing abuse, and ensuring a fair distribution of resources. Unthrottled requests can overwhelm servers, lead to denial-of-service attacks, and degrade user experience. This is where rate limiting steps in as a critical defensive mechanism. Among various rate limiting algorithms, the sliding window approach offers a more accurate and responsive way to control request rates compared to simpler methods like fixed windows or token buckets. This article will explore the implementation of a sliding window rate limiter using Go, renowned for its concurrency features and performance, and Redis, a powerful in-memory data store ideal for high-performance use cases like rate limiting. We'll delve into the mechanics of this robust technique and provide practical Go code examples to demonstrate its implementation.
Understanding the Core Concepts
Before diving into the implementation, let's establish a clear understanding of the key concepts involved:
- Rate Limiting: A control mechanism to limit the number of requests a user or client can make to a server within a given time frame. Its primary goals are to prevent resource exhaustion, protect against malicious activities, and ensure fair usage.
- Sliding Window Algorithm: This is a rate limiting algorithm that tracks requests over a continuous, moving time window. Unlike the fixed window algorithm, which can suffer from a "bursty" problem at the window boundaries, the sliding window provides a smoother and more accurate rate control. It typically combines two fixed windows: the current window and the previous window. The count for the current window is weighted by how much of that window has passed, and the count for the previous window is weighted by how much of that window is still relevant.
- Go (Golang): A statically typed, compiled programming language designed at Google. Its strengths lie in its excellent concurrency primitives (goroutines and channels), garbage collection, and robust standard library, making it an ideal choice for building high-performance network services.
- Redis: An open-source, in-memory data structure store, used as a database, cache, and message broker. Its lightning-fast read/write speeds, combined with support for various data structures like sorted sets and hash maps, make it exceptionally well-suited for implementing rate limiters.
The Sliding Window Rate Limiter Explained
The core idea of a sliding window rate limiter is to track requests by their timestamps. When a request arrives, its timestamp is added to a data structure. Then, to determine if the request should be allowed, we count the number of requests whose timestamps fall within the defined time window (e.g., the last 60 seconds). Crucially, as time progresses, older requests that fall outside the window are automatically discarded.
Redis's Sorted Sets (ZSETs) are perfectly suited for this. Each request's timestamp can be stored as the score, and a unique identifier (e.g., a UUID or simply the timestamp itself) can be the member. This allows us to efficiently:
- Add a new request's timestamp:
ZADD key timestamp timestamp - Remove old requests:
ZREMRANGEBYSCORE key -inf (now - windowDuration) - Count current requests:
ZCARD keyorZCOUNT key (now - windowDuration) +inf
Implementation Details with Go and Redis
Let's walk through the Go and Redis implementation.
First, we'll need a Redis client for Go. The go-redis/redis/v8 package is a popular and robust choice.
package main import ( "context" "fmt" "log" "strconv" "time" "github.com/go-redis/redis/v8" ) // RateLimiterConfig holds the configuration for our rate limiter type RateLimiterConfig struct { Limit int // Maximum requests allowed WindowSize time.Duration // The duration of the sliding window RedisClient *redis.Client } // NewRateLimiter creates a new RateLimiterConfig instance func NewRateLimiterConfig(limit int, windowSize time.Duration, rdb *redis.Client) *RateLimiterConfig { return &RateLimiterConfig{ Limit: limit, WindowSize: windowSize, RedisClient: rdb, } } // Allow checks if a request is allowed based on the sliding window algorithm func (rlc *RateLimiterConfig) Allow(ctx context.Context, key string) (bool, error) { now := time.Now().UnixNano() / int64(time.Millisecond) // Current timestamp in milliseconds // Use a Redis Transaction (MULTI/EXEC) for atomicity // This ensures that all operations are treated as a single, atomic unit pipe := rlc.RedisClient.Pipeline() // 1. Remove timestamps older than the window // ZREMRANGEBYSCORE key -inf (now - windowSizeInMilliseconds) // The `( ` before the score makes it exclusive. We want to remove elements *strictly older* than the window start. minScore := now - rlc.WindowSize.Milliseconds() pipe.ZRemRangeByScore(ctx, key, "-inf", strconv.FormatInt(minScore, 10)) // 2. Add the current request's timestamp // ZADD key now now // The score and member are the same here (timestamp) pipe.ZAdd(ctx, key, &redis.Z{ Score: float64(now), Member: now, }) // 3. Count the number of requests in the current window // ZCOUNT key (now - windowSizeInMilliseconds) +inf // We count all elements within the current window, including the newly added one. countCmd := pipe.ZCount(ctx, key, strconv.FormatInt(minScore, 10), "+inf") // 4. Set an expiration on the key to prevent it from growing indefinitely // This is important for keys that stop receiving traffic. // We set it to slightly longer than the window size to ensure records // are always available within the window, plus a small buffer. pipe.Expire(ctx, key, rlc.WindowSize+10*time.Second) // Add a buffer for safety // Execute all commands in the pipeline atomically _, err := pipe.Exec(ctx) if err != nil { return false, fmt.Errorf("redis transaction failed: %w", err) } // Get the count from the executed command currentRequests, err := countCmd.Result() if err != nil { return false, fmt.Errorf("failed to get request count from redis: %w", err) } return int(currentRequests) <= rlc.Limit, nil } func main() { // Initialize Redis client rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", // Replace with your Redis address Password: "", // No password set DB: 0, // Use default DB }) // Ping Redis to ensure connection ctx := context.Background() _, err := rdb.Ping(ctx).Result() if err != nil { log.Fatalf("Could not connect to Redis: %v", err) } fmt.Println("Connected to Redis!") // Configure the rate limiter: 5 requests per 10 seconds per unique key limiter := NewRateLimiterConfig(5, 10*time.Second, rdb) // Simulate requests for a specific user ID or API key userID := "user:123" fmt.Printf("Rate limit for %s: %d requests per %s\n", userID, limiter.Limit, limiter.WindowSize) for i := 1; i <= 10; i++ { allowed, err := limiter.Allow(ctx, userID) if err != nil { log.Printf("Error checking rate limit for %s: %v", userID, err) time.Sleep(500 * time.Millisecond) // Avoid hammering on error continue } if allowed { fmt.Printf("Request %d for %s: ALLOWED\n", i, userID) } else { fmt.Printf("Request %d for %s: BLOCKED (Rate Limit Exceeded)\n", i, userID) } time.Sleep(500 * time.Millisecond) // Simulate some delay between requests if i == 5 { fmt.Println("\n--- Waiting for window to slide ---") time.Sleep(6 * time.Second) // Wait for some time to see requests become allowed again fmt.Println("--- Continuing requests ---") } } }
Code Explanation:
RateLimiterConfigStructure: This struct holds theLimit(maximum requests allowed) andWindowSize(the duration of the sliding window, e.g., 10 seconds) and theRedisClientinstance.Allow(ctx context.Context, key string) (bool, error)Method: This is the core logic.now: We get the current timestamp in milliseconds, which serves as the score for our Redis Sorted Set members.- Redis Pipeline (Transaction): Crucially, we use a Redis
Pipelinefor atomicity. This bundles theZREMRANGEBYSCORE,ZADD,ZCOUNT, andEXPIREcommands into a single round trip to the Redis server. This guarantees that all these operations are executed sequentially and atomically from Redis's perspective, preventing race conditions where the count could be wrong between a read and a write. ZRemRangeByScore: This command removes all members from the sorted setkeywhose scores are less than or equal tonow - windowSize. This effectively prunes old requests that are no longer within our current sliding window.ZAdd: We add thenowtimestamp as both the score and member into the sorted set. The score being the timestamp allows us to sort and filter by time.ZCount: This command counts the number of members in the sorted setkeywhose scores are within the range(now - windowSize)to+inf. This gives us the total number of requests within the current sliding window.Expire: We set an expiration on the Redis key. This is a crucial optimization for keys that might stop receiving traffic. Without an expiration, unused rate limiter keys would accumulate in Redis memory indefinitely. We set it slightly longer thanWindowSizeto ensure that even requests at the very end of the previous window remain in the set long enough to be counted correctly when the window slides.Exec: Executes all the commands in the pipeline.currentRequests <= rlc.Limit: Finally, we compare the counted requests with our configuredLimitto determine if the incoming request should be allowed or blocked.
Application Scenarios
A sliding window rate limiter is highly versatile and can be applied in various scenarios:
- API Gateway Protection: Limiting the number of requests per client to protect backend services from being overwhelmed.
- User-specific Throttling: Preventing a single user from making too many requests (e.g., too many login attempts, too many search queries).
- DDOS Prevention: As a first line of defense against volumetric attacks by blocking IP addresses making an unusually high number of requests.
- Resource Fair Usage: Ensuring that all users get a fair share of limited resources by prioritizing those who adhere to the rate limits.
- Billing and Tiered Services: Implementing different rate limits for different subscription tiers (e.g., free tier gets 100 requests/minute, premium gets 1000 requests/minute).
Conclusion
Implementing a sliding window rate limiter with Go and Redis provides a highly effective and efficient way to manage API traffic. By leveraging Redis's Sorted Sets and Go's concurrency capabilities, we can build a robust system that accurately tracks requests over a continuous time window, preventing resource exhaustion and ensuring system stability. This approach offers superior fairness and accuracy compared to simpler methods, making it an indispensable tool for designing resilient distributed applications.

