Rate Limiting: Your API's First Line of Defense
Without rate limiting, a single user (or bot) can overwhelm your API with thousands of requests per second. Rate limiting caps the number of requests a client can make in a time window.
Common Algorithms
| Algorithm | How It Works | Pros | Cons |
|---|
| Fixed Window | Count requests per fixed time window (e.g., per minute) | Simple to implement | Burst at window boundaries |
| Sliding Window | Count requests in a rolling time window | Smooth distribution | More memory |
| Token Bucket | Tokens refill at a fixed rate; each request costs a token | Allows controlled bursts | Slightly complex |
| Leaky Bucket | Requests queue and process at a fixed rate | Smooth output rate | Drops requests when full |
Response Headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 100 # max requests per window
X-RateLimit-Remaining: 42 # requests left
X-RateLimit-Reset: 1682345700 # when the window resets (Unix)
Retry-After: 30 # seconds to wait (on 429)
HTTP/1.1 429 Too Many Requests
Retry-After: 30
{
"error": "Rate limit exceeded",
"retry_after": 30
}
Express.js with Redis
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per window
standardHeaders: true,
legacyHeaders: false,
store: new RedisStore({
sendCommand: (...args) => redis.call(...args),
}),
message: {
error: 'Too many requests, please try again later',
},
});
app.use('/api/', limiter);
// Different limits for different endpoints
const strictLimiter = rateLimit({
windowMs: 60 * 1000,
max: 10, // only 10 requests per minute
});
app.post('/api/auth/login', strictLimiter, loginHandler);
DIY Sliding Window with Redis
async function checkRateLimit(userId, limit = 100, window = 60) {
const key = `rate:${userId}`;
const now = Date.now();
const windowStart = now - window * 1000;
const pipeline = redis.pipeline();
pipeline.zremrangebyscore(key, 0, windowStart); // remove old entries
pipeline.zadd(key, now, `${now}:${Math.random()}`); // add current
pipeline.zcard(key); // count entries in window
pipeline.expire(key, window); // auto-cleanup
const results = await pipeline.exec();
const count = results[2][1];
return {
allowed: count <= limit,
remaining: Math.max(0, limit - count),
reset: Math.ceil(windowStart / 1000) + window,
};
}
Rate Limiting by Key
| Key By | Use Case |
|---|
| IP address | Anonymous endpoints, public APIs |
| API key | Authenticated APIs with API keys |
| User ID | Authenticated users |
| IP + endpoint | Per-route limits for unauthenticated users |
Best Practices
- Return
429 status code with Retry-After header - Include rate limit headers in every response (not just 429)
- Use different limits for different endpoints (login vs read)
- Log rate limit hits for abuse detection
- Consider geographic or plan-based limits