API Rate Limiting Strategies
Token bucket, sliding window, Redis-backed, and per-user limits
What You’ll Build
After following this guide, you will have a working implementation of api rate limiting strategies in your project. Protect your API from abuse with rate limiting. Covers the token bucket algorithm (best for bursty traffic), sliding window (most accurate), Redis-backed limiting for distributed systems, and per-user/per-IP differentiation.
Use Cases & Problems Solved
- Build reliable server endpoints that clients can consume consistently
- Handle common backend patterns like routing, middleware, and error handling
- Provide a clean interface between your frontend and data layer
Prerequisites
- Node.js 18+
- Redis for distributed rate limiting
Step-by-Step Implementation
Simple in-memory rate limiter
The following snippet shows how to simple in-memory rate limiter. Copy this into your project and adjust the values for your environment.
npm install express-rate-limit
Basic setup with per-route limits
The following snippet shows how to basic setup with per-route limits. Copy this into your project and adjust the values for your environment.
const rateLimit = require('express-rate-limit');
// Global limiter
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
// Strict limiter for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: { error: 'Too many login attempts, please try again in 15 minutes' },
standardHeaders: true,
legacyHeaders: false,
});
app.post('/auth/login', authLimiter, loginHandler);
Redis-backed for distributed systems
The following snippet shows how to redis-backed for distributed systems. Copy this into your project and adjust the values for your environment.
const { RedisStore } = require('rate-limit-redis');
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
const apiLimiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
windowMs: 60 * 1000,
max: 60,
keyGenerator: (req) => req.user?.id || req.ip, // per-user if authenticated
});
app.use('/api/', apiLimiter);
⚠️ Don’t Do This
❌ Using in-memory rate limiting in a multi-server deployment
// Each server has its own counter — user can hit each server separately!
app.use(rateLimit({ max: 100 })); // 3 servers = 300 effective requests!
✅ Use Redis-backed rate limiting for shared state across servers
app.use(rateLimit({
store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
max: 100 // shared across ALL servers
}));
Testing
Add these tests to verify your API endpoints work correctly:
// __tests__/api.test.ts
import { describe, it, expect } from 'vitest';
describe('API Rate Limiting Strategies', () => {
it('should return 200 for valid requests', async () => {
const res = await fetch('/api/endpoint', { method: 'GET' });
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toBeDefined();
});
it('should return 400 for invalid input', async () => {
const res = await fetch('/api/endpoint', {
method: 'POST',
body: JSON.stringify({}),
});
expect(res.status).toBe(400);
});
it('should handle errors gracefully', async () => {
const res = await fetch('/api/endpoint/nonexistent');
expect(res.status).toBe(404);
});
});
Verification
# Test rate limiting:
for i in $(seq 1 10); do
curl -s -o /dev/null -w '%{http_code}\
' http://localhost:3000/auth/login \
-X POST -H 'Content-Type: application/json' -d '{"email":"test"}'
done
# First 5: 200/401, remaining: 429 Too Many Requests Related Specs
Secure Webhook Handler
Signature verification, idempotency, retry handling, and queue processing
Background Jobs with BullMQ
Queue setup, workers, retries, job scheduling, and monitoring dashboard
GraphQL API with Apollo Server
Schema, resolvers, context, dataloaders, and error handling