Implementing Sliding Window Rate Limiting

Implementing Sliding Window Rate Limiting

Sliding window algorithms provide more accurate rate limiting than fixed windows by considering a continuous time period rather than discrete intervals. This approach prevents burst attacks at window boundaries and provides smoother rate limiting behavior. Several sliding window implementations offer different trade-offs between accuracy and computational complexity.

// Node.js implementation of sliding window rate limiter using Redis
const redis = require('redis');
const client = redis.createClient();

class SlidingWindowRateLimiter {
    constructor(windowSize, limit) {
        this.windowSize = windowSize; // in seconds
        this.limit = limit;
    }
    
    async checkLimit(clientId) {
        const now = Date.now();
        const windowStart = now - (this.windowSize * 1000);
        const key = `rate_limit:${clientId}`;
        
        // Remove old entries
        await client.zremrangebyscore(key, '-inf', windowStart);
        
        // Count requests in current window
        const count = await client.zcard(key);
        
        if (count < this.limit) {
            // Add current request
            await client.zadd(key, now, `${now}-${Math.random()}`);
            await client.expire(key, this.windowSize);
            
            return {
                allowed: true,
                remaining: this.limit - count - 1,
                resetAt: now + (this.windowSize * 1000)
            };
        }
        
        // Calculate when oldest request will expire
        const oldestRequest = await client.zrange(key, 0, 0, 'WITHSCORES');
        const retryAfter = oldestRequest[1] ? 
            Math.ceil((parseInt(oldestRequest[1]) + (this.windowSize * 1000) - now) / 1000) : 
            this.windowSize;
        
        return {
            allowed: false,
            remaining: 0,
            resetAt: now + (retryAfter * 1000),
            retryAfter: retryAfter
        };
    }
}

// Express middleware
const rateLimiter = new SlidingWindowRateLimiter(3600, 1000); // 1000 requests per hour

async function rateLimitMiddleware(req, res, next) {
    const clientId = req.headers['x-api-key'] || req.ip;
    const result = await rateLimiter.checkLimit(clientId);
    
    res.set({
        'X-RateLimit-Limit': rateLimiter.limit,
        'X-RateLimit-Remaining': result.remaining,
        'X-RateLimit-Reset': new Date(result.resetAt).toISOString()
    });
    
    if (!result.allowed) {
        res.set('Retry-After', result.retryAfter);
        return res.status(429).json({
            error: 'Too Many Requests',
            message: 'Rate limit exceeded',
            retryAfter: result.retryAfter
        });
    }
    
    next();
}

Distributed rate limiting becomes essential when APIs run across multiple servers. Centralized storage in Redis or similar systems enables consistent rate limiting across all API instances. Consider eventual consistency trade-offs when designing distributed rate limiters. Implement local caching with conservative limits to handle temporary connectivity issues with the central store.