Rate Limiting and Throttling

Rate Limiting and Throttling

Rate limiting protects APIs from abuse, whether intentional attacks or unintended overuse. Without proper rate limiting, APIs face denial-of-service attacks, resource exhaustion, and inflated infrastructure costs. Effective rate limiting requires balancing security with legitimate usage patterns, implementing different limits for different operations and user tiers.

Token bucket algorithms provide flexible rate limiting that allows burst traffic while maintaining overall limits. Sliding window approaches offer more precise control but require more computational resources. Distributed rate limiting across multiple API servers requires coordination through shared storage like Redis, adding complexity but ensuring consistent enforcement.

// Example: Advanced rate limiting for APIs
const Redis = require('ioredis');
const crypto = require('crypto');

class AdvancedRateLimiter {
    constructor(redisConfig) {
        this.redis = new Redis({
            ...redisConfig,
            enableOfflineQueue: false,
            maxRetriesPerRequest: 3
        });
        
        // Define rate limit tiers
        this.tiers = {
            anonymous: {
                requests: 100,
                window: 3600, // 1 hour
                burst: 10
            },
            authenticated: {
                requests: 1000,
                window: 3600,
                burst: 50
            },
            premium: {
                requests: 10000,
                window: 3600,
                burst: 200
            }
        };
        
        // Operation-specific limits
        this.operationLimits = {
            'GET /api/users': { multiplier: 1 },
            'POST /api/users': { multiplier: 10 },
            'POST /api/auth/login': { multiplier: 5, custom: { requests: 5, window: 300 } },
            'POST /api/data/export': { multiplier: 100 }
        };
    }
    
    async checkLimit(identifier, tier = 'anonymous', operation = null) {
        const limits = this.getTierLimits(tier);
        const operationConfig = operation ? this.operationLimits[operation] : null;
        
        // Apply operation-specific limits if defined
        if (operationConfig?.custom) {
            Object.assign(limits, operationConfig.custom);
        }
        
        const cost = operationConfig?.multiplier || 1;
        
        // Use sliding window algorithm
        const result = await this.slidingWindowCheck(identifier, limits, cost);
        
        if (!result.allowed) {
            // Calculate retry-after
            result.retryAfter = await this.calculateRetryAfter(identifier, limits);
            
            // Log rate limit violation
            await this.logViolation(identifier, operation, result);
        }
        
        return result;
    }
    
    async slidingWindowCheck(identifier, limits, cost = 1) {
        const now = Date.now();
        const windowStart = now - (limits.window * 1000);
        const key = `rate_limit:${identifier}`;
        
        // Lua script for atomic sliding window operation
        const luaScript = `
            local key = KEYS[1]
            local now = tonumber(ARGV[1])
            local window_start = tonumber(ARGV[2])
            local limit = tonumber(ARGV[3])
            local cost = tonumber(ARGV[4])
            local expire = tonumber(ARGV[5])
            
            -- Remove old entries
            redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
            
            -- Count current requests in window
            local current = redis.call('ZCARD', key)
            
            -- Check if limit would be exceeded
            if current + cost > limit then
                return {0, current, limit}
            end
            
            -- Add new request
            redis.call('ZADD', key, now, now .. ':' .. math.random())
            redis.call('EXPIRE', key, expire)
            
            return {1, current + cost, limit}
        `;
        
        const result = await this.redis.eval(
            luaScript,
            1,
            key,
            now,
            windowStart,
            limits.requests,
            cost,
            limits.window
        );
        
        return {
            allowed: result[0] === 1,
            current: result[1],
            limit: result[2],
            remaining: Math.max(0, result[2] - result[1]),
            reset: now + (limits.window * 1000)
        };
    }
    
    async implementDistributedRateLimit(req, res, next) {
        // Extract identifier based on authentication
        const identifier = this.extractIdentifier(req);
        const tier = req.user ? req.user.tier : 'anonymous';
        const operation = `${req.method} ${req.route.path}`;
        
        try {
            const result = await this.checkLimit(identifier, tier, operation);
            
            // Set rate limit headers
            res.set({
                'X-RateLimit-Limit': result.limit,
                'X-RateLimit-Remaining': result.remaining,
                'X-RateLimit-Reset': result.reset,
                'X-RateLimit-Reset-After': Math.ceil((result.reset - Date.now()) / 1000)
            });
            
            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();
        } catch (error) {
            // Redis failure - fail open or closed based on config
            if (this.config.failClosed) {
                return res.status(503).json({
                    error: 'Service Unavailable',
                    message: 'Unable to process request'
                });
            }
            
            // Fail open - allow request but log error
            console.error('Rate limiting error:', error);
            next();
        }
    }
    
    extractIdentifier(req) {
        // Authenticated user
        if (req.user) {
            return `user:${req.user.id}`;
        }
        
        // API key
        if (req.headers['x-api-key']) {
            return `api_key:${crypto.createHash('sha256')
                .update(req.headers['x-api-key'])
                .digest('hex')}`;
        }
        
        // IP address (with privacy consideration)
        const ip = req.ip || req.connection.remoteAddress;
        return `ip:${crypto.createHash('sha256')
            .update(ip + this.config.ipSalt)
            .digest('hex').substring(0, 16)}`;
    }
    
    async detectAndMitigateAbuse(identifier) {
        // Pattern detection for abuse
        const patterns = await this.analyzeRequestPatterns(identifier);
        
        if (patterns.suspicious) {
            // Implement progressive penalties
            if (patterns.score > 0.8) {
                // Temporary ban
                await this.temporaryBan(identifier, 3600); // 1 hour
            } else if (patterns.score > 0.6) {
                // Increase rate limit restrictions
                await this.tightenLimits(identifier, 0.5); // 50% reduction
            } else {
                // Add to watchlist
                await this.addToWatchlist(identifier);
            }
        }
    }
}