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);
}
}
}
}