Rate Limiting Without the Headaches
A practical guide to implementing rate limiting that protects your API without punishing legitimate users.
Rate limiting is one of those features everyone knows they need but few implement well. Done wrong, it blocks real users. Done right, it's invisible.
Choose the right algorithm
| Algorithm | Best for |
|---|---|
| Fixed window | Simple limits, low traffic |
| Sliding window | Smooth limiting, API endpoints |
| Token bucket | Burst tolerance, user-facing features |
For most APIs, sliding window hits the sweet spot between accuracy and simplicity.
Key design decisions
Identifier strategy
Rate limit by what matters for your threat model:
- IP address — anonymous endpoints
- User ID — authenticated routes
- API key — third-party integrations
Response headers
Always return standard headers so clients can self-regulate:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1698765432
Retry-After: 60
Graceful degradation
When Redis is down, fail open for read endpoints and fail closed for write endpoints. Document this behavior explicitly.
Middleware pattern
Keep rate limiting as middleware, not scattered across handlers:
app.use("/api/*", rateLimit({
window: "1m",
max: 100,
key: (req) => req.user?.id ?? req.ip,
}));One place to configure, one place to debug.
Testing rate limits
Don't skip testing the limit itself. Write integration tests that:
- Make requests up to the limit
- Verify the 429 response
- Confirm headers are correct
- Wait for reset and verify recovery
Rate limiting done well protects your infrastructure while staying completely invisible to users who play by the rules.