Rate Limiting
Protect your API from abuse - rate limiting strategies, implementations, and best practices.
6 min read
Why Rate Limit?#
Without rate limiting, your API is vulnerable to:
- DDoS attacks - Overwhelm your servers
- Brute force - Password guessing
- Scraping - Stealing your data
- Accidental abuse - Buggy clients
- Cost overruns - Expensive operations
Rate Limiting Strategies#
| Strategy | Description | Best For |
|---|---|---|
| Fixed Window | X requests per minute | Simple, general use |
| Sliding Window | Rolling time window | More accurate |
| Token Bucket | Tokens refill over time | Burst handling |
| Leaky Bucket | Constant rate output | Smoothing traffic |
Option 1: express-rate-limit (Simple)#
bash
npm install express-rate-limit
Basic Setup#
javascript
import rateLimit from 'express-rate-limit';
// General API limiter
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: {
error: {
message: 'Too many requests, please try again later',
code: 'RATE_LIMITED',
},
},
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false,
});
app.use('/api', apiLimiter);
Different Limits for Different Routes#
javascript
// Strict limit for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // Only 5 attempts
message: {
error: {
message: 'Too many login attempts, try again in 15 minutes',
code: 'AUTH_RATE_LIMITED',
},
},
});
// Relaxed limit for read operations
const readLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 60, // 60 requests per minute
});
// Very strict for expensive operations
const expensiveLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10, // 10 per hour
});
// Apply to routes
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);
app.use('/api/products', readLimiter);
app.use('/api/reports/generate', expensiveLimiter);
Per-User Rate Limiting#
javascript
const userLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
keyGenerator: (req) => {
// Rate limit by user ID (authenticated) or IP (anonymous)
return req.user?.id || req.ip;
},
skip: (req) => {
// Skip rate limiting for admins
return req.user?.role === 'admin';
},
});
Option 2: rate-limiter-flexible (Advanced)#
More features, Redis support for distributed systems:
bash
npm install rate-limiter-flexible
Redis-backed Rate Limiter#
javascript
import { RateLimiterRedis } from 'rate-limiter-flexible';
import { createClient } from 'redis';
const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();
const rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'rl',
points: 100, // 100 requests
duration: 60, // Per 60 seconds
blockDuration: 60 * 5, // Block for 5 minutes if exceeded
});
// Middleware
export async function rateLimitMiddleware(req, res, next) {
const key = req.user?.id || req.ip;
try {
const result = await rateLimiter.consume(key);
// Add headers
res.set('X-RateLimit-Limit', 100);
res.set('X-RateLimit-Remaining', result.remainingPoints);
res.set('X-RateLimit-Reset', new Date(Date.now() + result.msBeforeNext));
next();
} catch (error) {
if (error.remainingPoints !== undefined) {
// Rate limited
res.set('Retry-After', Math.ceil(error.msBeforeNext / 1000));
res.status(429).json({
error: {
message: 'Too many requests',
code: 'RATE_LIMITED',
retryAfter: Math.ceil(error.msBeforeNext / 1000),
},
});
} else {
// Redis error - fail open
next();
}
}
}
Different Limits by Plan#
javascript
const rateLimiters = {
free: new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'rl:free',
points: 100,
duration: 60 * 60, // 100 per hour
}),
pro: new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'rl:pro',
points: 1000,
duration: 60 * 60, // 1000 per hour
}),
enterprise: new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'rl:enterprise',
points: 10000,
duration: 60 * 60, // 10000 per hour
}),
};
export async function tieredRateLimit(req, res, next) {
const plan = req.user?.plan || 'free';
const limiter = rateLimiters[plan];
try {
await limiter.consume(req.user?.id || req.ip);
next();
} catch (error) {
res.status(429).json({
error: {
message: 'Rate limit exceeded',
code: 'RATE_LIMITED',
upgrade: plan === 'free' ? 'Upgrade to Pro for higher limits' : null,
},
});
}
}
Sliding Window#
javascript
import { RateLimiterRedis } from 'rate-limiter-flexible';
const slidingWindowLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'rl:sliding',
points: 100,
duration: 60,
execEvenly: true, // Smooth out requests
});
API Key Rate Limiting#
javascript
// Different limits based on API key
const apiKeyLimits = {
default: { points: 100, duration: 3600 },
premium: { points: 10000, duration: 3600 },
};
export async function apiKeyRateLimit(req, res, next) {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({ error: 'API key required' });
}
// Get key info from database
const keyInfo = await ApiKey.findOne({ key: apiKey });
if (!keyInfo) {
return res.status(401).json({ error: 'Invalid API key' });
}
const limits = apiKeyLimits[keyInfo.tier] || apiKeyLimits.default;
const limiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: `rl:api:${keyInfo.tier}`,
points: limits.points,
duration: limits.duration,
});
try {
const result = await limiter.consume(apiKey);
res.set('X-RateLimit-Limit', limits.points);
res.set('X-RateLimit-Remaining', result.remainingPoints);
req.apiKey = keyInfo;
next();
} catch (error) {
res.status(429).json({
error: {
message: 'Rate limit exceeded',
limit: limits.points,
resetAt: new Date(Date.now() + error.msBeforeNext),
},
});
}
}
Response Headers#
Standard rate limit headers:
javascript
// Response headers
res.set('X-RateLimit-Limit', '100'); // Max requests
res.set('X-RateLimit-Remaining', '95'); // Requests left
res.set('X-RateLimit-Reset', '1640000000'); // Unix timestamp when resets
res.set('Retry-After', '60'); // Seconds until can retry (429 only)
Handling Rate Limit Errors#
javascript
// Client-side handling
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 60;
console.log(`Rate limited. Retrying in ${retryAfter}s`);
await sleep(retryAfter * 1000);
continue;
}
return response;
}
throw new Error('Max retries exceeded');
}
Best Practices#
1. Fail Open#
javascript
try {
await rateLimiter.consume(key);
next();
} catch (error) {
if (error instanceof Error) {
// Redis down - allow request (fail open)
console.error('Rate limiter error:', error);
next();
} else {
// Actually rate limited
res.status(429).json({ error: 'Too many requests' });
}
}
2. Whitelist Trusted IPs#
javascript
const trustedIPs = new Set(['10.0.0.1', '192.168.1.1']);
export function rateLimitMiddleware(req, res, next) {
if (trustedIPs.has(req.ip)) {
return next(); // Skip rate limiting
}
// Apply rate limiting...
}
3. Separate Limits by Operation#
javascript
// Read operations - higher limits
app.get('/api/*', readLimiter);
// Write operations - lower limits
app.post('/api/*', writeLimiter);
app.put('/api/*', writeLimiter);
app.delete('/api/*', writeLimiter);
Key Takeaways#
- Always rate limit - Protect against abuse
- Use Redis for distributed - Share limits across servers
- Different limits for different routes - Auth stricter than reads
- Return proper headers - Help clients handle limits
- Fail open - Don't break if Redis is down
Continue Learning
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.