Rate Limiting Algorithms
Fixed Window
Simple but allows burst at window boundaries.
Sliding Window
Smoother distribution, counts requests in a rolling window.
Token Bucket
Allows controlled bursts while enforcing average rate.
express-rate-limit (Basic)
npm install express-rate-limit
import rateLimit from 'express-rate-limit'
// Global rate limit
app.use(rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests, please try again later.' },
}))
// Strict limit for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true, // Only count failed attempts
})
app.use('/auth/login', authLimiter)
Redis-Backed Sliding Window
import Redis from 'ioredis'
const redis = new Redis()
async function slidingWindowLimit(
key: string,
limit: number,
windowMs: number
): Promise<{ allowed: boolean; remaining: number; resetAt: Date }> {
const now = Date.now()
const windowStart = now - windowMs
const pipeline = redis.pipeline()
pipeline.zremrangebyscore(key, '-inf', windowStart) // Remove old entries
pipeline.zadd(key, now, `${now}-${Math.random()}`) // Add current request
pipeline.zcard(key) // Count in window
pipeline.pexpire(key, windowMs) // Set expiry
const results = await pipeline.exec()
const count = results?.[2]?.[1] as number ?? 0
return {
allowed: count <= limit,
remaining: Math.max(0, limit - count),
resetAt: new Date(now + windowMs),
}
}
// Middleware
app.use(async (req, res, next) => {
const identifier = req.user?.id ?? req.ip
const { allowed, remaining, resetAt } = await slidingWindowLimit(
`rate:${identifier}`,
100,
60 * 1000
)
res.set({
'X-RateLimit-Limit': '100',
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': resetAt.toISOString(),
})
if (!allowed) {
return res.status(429).json({ error: 'Rate limit exceeded' })
}
next()
})
Token Bucket Algorithm
class TokenBucket {
private tokens: number
private lastRefill: number
constructor(
private capacity: number,
private refillRate: number, // tokens per second
) {
this.tokens = capacity
this.lastRefill = Date.now()
}
async consume(tokens = 1): Promise<boolean> {
this.refill()
if (this.tokens < tokens) return false
this.tokens -= tokens
return true
}
private refill() {
const now = Date.now()
const elapsed = (now - this.lastRefill) / 1000
this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate)
this.lastRefill = now
}
}
// Distributed token bucket with Redis Lua script
const consumeToken = `
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local bucket = redis.call('hmget', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now
local elapsed = (now - last_refill) / 1000
tokens = math.min(capacity, tokens + elapsed * refill_rate)
if tokens >= requested then
tokens = tokens - requested
redis.call('hmset', key, 'tokens', tokens, 'last_refill', now)
redis.call('pexpire', key, 60000)
return 1
end
return 0
`
async function consumeTokenRedis(userId: string): Promise<boolean> {
const result = await redis.eval(
consumeToken, 1,
`bucket:${userId}`,
100, // capacity
10, // refill rate (tokens/sec)
Date.now(),
1 // consume 1 token
)
return result === 1
}
Tiered Rate Limits
const tiers = {
free: { requests: 100, windowMs: 60 * 60 * 1000 },
pro: { requests: 1000, windowMs: 60 * 60 * 1000 },
enterprise: { requests: 10000, windowMs: 60 * 60 * 1000 },
}
app.use(async (req, res, next) => {
const tier = req.user?.subscription ?? 'free'
const { requests, windowMs } = tiers[tier]
const { allowed } = await slidingWindowLimit(`rate:${req.user?.id}`, requests, windowMs)
if (!allowed) return res.status(429).json({ error: 'Rate limit exceeded' })
next()
})