Rate Limiting in APIs: Protection Against Abuse
Rate limiting is an essential technique for protecting APIs against abuse, excessive resource consumption and denial-of-service attacks. A proper implementation ensures availability for legitimate users while blocking malicious or excessive behavior.
Why Implement Rate Limiting?
- Protection Against DDoS: Mitigate denial-of-service attacks
- Prevent Scraping: Hinder automated data extraction
- Control Costs: Avoid excessive consumption of computing resources
- Ensure Quality of Service: Distribute resources fairly
- Prevent Brute Force: Limit authentication attempts
Rate Limiting Algorithms
1. Token Bucket
An algorithm that maintains a "bucket" of tokens that refill over time:
class TokenBucket {
constructor(capacity, refillRate) {
this.capacity = capacity; // Capacidade máxima do balde
this.tokens = capacity; // Tokens disponíveis
this.refillRate = refillRate; // Tokens por segundo
this.lastRefill = Date.now();
}
tryConsume(tokens = 1) {
this.refill();
if (this.tokens >= tokens) {
this.tokens -= tokens;
return true; // Requisição permitida
}
return false; // Rate limit excedido
}
refill() {
const now = Date.now();
const timePassed = (now - this.lastRefill) / 1000;
const tokensToAdd = timePassed * this.refillRate;
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
this.lastRefill = now;
}
}
// Uso: 100 requisições máximo, recarrega 10/segundo
const bucket = new TokenBucket(100, 10);
Advantages: Allows controlled bursts, smooths traffic
Disadvantages: More complex to implement
2. Leaky Bucket
Processes requests at a constant rate, like water leaking from a bucket:
- Requests enter the bucket
- Processed at a fixed rate
- Excess overflows (rejected)
- Ensures uniform output
3. Fixed Window
Counts requests within fixed time windows:
class FixedWindowRateLimiter {
constructor(maxRequests, windowMs) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.requests = new Map();
}
isAllowed(userId) {
const now = Date.now();
const windowStart = Math.floor(now / this.windowMs) * this.windowMs;
const key = \`\$:\$\`;
const count = this.requests.get(key) || 0;
if (count < this.maxRequests) {
this.requests.set(key, count + 1);
return true;
}
return false;
}
}
// 100 requisições por hora
const limiter = new FixedWindowRateLimiter(100, 60 * 60 * 1000);
Problem: Allows up to 2x the limit at window edges
4. Sliding Window Log
Maintains a log of request timestamps:
- Stores a timestamp for each request
- Removes requests outside the window
- More precise than Fixed Window
- Higher memory consumption
5. Sliding Window Counter
Combines Fixed Window with smoothing:
// Calcula uma média ponderada entre janelas atual e anterior
const currentWindowCount = getCurrentWindowCount(userId);
const previousWindowCount = getPreviousWindowCount(userId);
const percentageInCurrentWindow = (now - currentWindowStart) / windowSize;
const estimatedCount =
previousWindowCount * (1 - percentageInCurrentWindow) +
currentWindowCount;
return estimatedCount < maxRequests;
Practical Implementation
With Redis (Recommended for Production)
import Redis from 'ioredis';
const redis = new Redis();
async function checkRateLimit(userId, maxRequests = 100, windowSeconds = 60) {
const key = \`rate_limit:\$\`;
const now = Date.now();
const windowStart = now - (windowSeconds * 1000);
// Remover requisições antigas
await redis.zremrangebyscore(key, 0, windowStart);
// Contar requisições na janela
const requestCount = await redis.zcard(key);
if (requestCount < maxRequests) {
// Adicionar nova requisição
await redis.zadd(key, now, \`\$-\${Math.random()}\`);
await redis.expire(key, windowSeconds);
return { allowed: true, remaining: maxRequests - requestCount - 1 };
}
return { allowed: false, remaining: 0 };
}
// Middleware Express
app.use(async (req, res, next) => {
const userId = req.user?.id || req.ip;
const result = await checkRateLimit(userId);
res.set({
'X-RateLimit-Limit': 100,
'X-RateLimit-Remaining': result.remaining,
'X-RateLimit-Reset': new Date(Date.now() + 60000).toISOString()
});
if (!result.allowed) {
return res.status(429).json({
error: 'Too Many Requests',
retryAfter: 60
});
}
next();
});
Popular Libraries
- express-rate-limit: Middleware for Express.js
- rate-limiter-flexible: Supports multiple backends (Redis, Memcached, MySQL)
- Kong Rate Limiting: Plugin for API Gateway
- AWS API Gateway: Native rate limiting
Advanced Strategies
Hierarchical Rate Limiting
- Global: Total API limit (e.g.: 1M req/min)
- Per User: Individual limit (e.g.: 1000 req/min)
- Per Endpoint: Specific limits (login: 5 req/min)
- Per IP: Additional protection against abuse
Dynamic Rate Limiting
- Adjust limits based on system load
- Increase limits for premium users
- Reduce limits during incidents
Whitelisting and Blacklisting
- Exempt trusted IPs/users
- Permanently block known attackers
- Implement a reputation system
Best Practices
- Return informative headers (X-RateLimit-*)
- Use HTTP status 429 (Too Many Requests)
- Include a Retry-After header
- Document limits clearly in the API
- Implement exponential backoff on the client
- Monitor rate limiting metrics
- Alert on abnormal patterns
- Test limits before production
Monitoring Tools
- Grafana + Prometheus: Visualize rate limiting metrics
- Datadog: Monitoring and alerts
- CloudWatch: For APIs on AWS
- New Relic: APM with rate limiting support
