Rate Limiting
Rate limiting caps how many requests a client may make in a window of time. It protects your API from abuse (brute-force logins, scraping), accidental overload (a buggy client in a loop), and runaway costs — and it keeps one heavy user from degrading service for everyone. This page covers the algorithms, the express-rate-limit middleware, the 429 response, and the production gotchas around proxies and multiple instances.
The algorithms
Algorithm | How it works | Trade-off |
|---|---|---|
Fixed window | N requests per clock window (e.g. per minute) | Simple; allows bursts at window edges |
Sliding window | Rolling time window, smoother | More accurate; more state |
Token bucket | Tokens refill at a rate; each request spends one | Allows controlled bursts |
Leaky bucket | Requests drain at a fixed rate | Smooths output, queues bursts |
Basic setup with express-rate-limit
import rateLimit from 'express-rate-limit'
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per IP per window
standardHeaders: 'draft-7', // send RateLimit-* headers
legacyHeaders: false,
message: { error: { code: 'RATE_LIMITED', message: 'Too many requests' } },
})
app.use('/api', limiter)Tighter limits on sensitive routes
// Brute-force defense: only a handful of login attempts per window.
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true, // only count failed logins
})
app.post('/auth/login', loginLimiter, loginHandler)
// Expensive endpoints (search, export) get their own stricter cap:
app.use('/api/search', rateLimit({ windowMs: 60_000, max: 10 }))The 429 response
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 42 ← seconds until the window resets
Retry-After: 42
{ "error": { "code": "RATE_LIMITED", "message": "Too many requests" } }Trust proxy — get the real client IP
// Behind nginx, a load balancer, or a CDN, the socket IP is the PROXY's.
// Tell Express to read the client IP from X-Forwarded-For:
app.set('trust proxy', 1) // number = how many proxies to trust
// Now req.ip (and the limiter's key) is the real client, not the proxy.Multiple instances need a shared store
import RedisStore from 'rate-limit-redis'
import { createClient } from 'redis'
const redis = createClient({ url: process.env.REDIS_URL })
await redis.connect()
const limiter = rateLimit({
windowMs: 60_000,
max: 100,
store: new RedisStore({ sendCommand: (...args) => redis.sendCommand(args) }),
})Key by user, not just IP, when authenticated
const limiter = rateLimit({
windowMs: 60_000,
max: 1000,
// Authenticated users limited per account; anonymous, per IP:
keyGenerator: (req) => req.user?.id ?? req.ip,
})