Rate Limiting Strategies: Token Bucket, Sliding Window, and Fixed Window
Rate limiting protects your API from abuse, accidental DDoS, and runaway client bugs. Three algorithms dominate — here's when to use each.
Fixed Window
Count requests in fixed time windows (0:00-1:00, 1:00-2:00).
Window: 60 seconds, limit: 100 requests
00:00-01:00: 100 requests → blocked
01:00-02:00: counter resets → 100 more allowed
Problem: 100 requests at 00:59 + 100 at 01:00 = 200 requests in 2 seconds
// Fixed window with Redis
async function fixedWindow(key: string, limit: number, windowMs: number) {
const windowKey = `${key}:${Math.floor(Date.now() / windowMs)}`;
const count = await redis.incr(windowKey);
if (count === 1) await redis.pexpire(windowKey, windowMs);
return count <= limit;
}
Sliding Window
Track requests in a rolling window — no reset spike problem.
// Sliding window log
async function slidingWindow(key: string, limit: number, windowMs: number) {
const now = Date.now();
const windowStart = now - windowMs;
// Remove old requests
await redis.zremrangebyscore(key, '-inf', windowStart);
// Count requests in window
const count = await redis.zcard(key);
if (count >= limit) return false;
// Add current request
await redis.zadd(key, now, `${now}-${Math.random()}`);
await redis.pexpire(key, windowMs);
return true;
}
Token Bucket
Tokens refill at a fixed rate. Requests consume tokens. Allows bursts up to bucket capacity.
// Token bucket — allows bursting
async function tokenBucket(
key: string,
capacity: number, // Max tokens (burst limit)
refillRate: number, // Tokens per second
) {
const now = Date.now() / 1000; // seconds
const bucket = await redis.hgetall(key);
const tokens = parseFloat(bucket?.tokens ?? capacity.toString());
const lastRefill = parseFloat(bucket?.lastRefill ?? now.toString());
// Calculate new tokens
const elapsed = now - lastRefill;
const newTokens = Math.min(capacity, tokens + elapsed * refillRate);
if (newTokens < 1) return false; // No tokens available
// Consume a token
await redis.hset(key, { tokens: newTokens - 1, lastRefill: now });
await redis.expire(key, 3600);
return true;
}
Express Middleware
function rateLimitMiddleware(limit: number, windowMs: number) {
return async (req: Request, res: Response, next: NextFunction) => {
const key = `rate:${req.ip}`;
const allowed = await slidingWindow(key, limit, windowMs);
if (!allowed) {
return res.status(429).json({
error: 'Too many requests',
retryAfter: Math.ceil(windowMs / 1000),
});
}
next();
};
}
// Apply globally or per-route
app.use('/api', rateLimitMiddleware(100, 60_000)); // 100 req/min
app.use('/api/auth', rateLimitMiddleware(10, 60_000)); // Stricter for auth
Using @upstash/ratelimit (Easiest)
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(100, '1 m'),
analytics: true,
});
const { success, limit, remaining, reset } = await ratelimit.limit(req.ip);
Decision Guide
| Use Case | Algorithm |
|---|---|
| Simple API protection | Fixed window |
| No burst spikes | Sliding window |
| Allow burst traffic | Token bucket |
| Edge/serverless | @upstash/ratelimit |
Rate limiting ships pre-configured (sliding window + Upstash) in the AI SaaS Starter Kit. $99 at whoffagents.com.
Top comments (0)