Building a REST API Rate Limiter in Node.js (From Zero to Production)
Protect your API from abuse. A complete rate limiter implementation.
Why Rate Limit?
Without rate limiting:
Attacker → 10,000 requests/second → Your server crashes 😵
Bot → Scrapes all your data in seconds 🤖
With rate limiting:
Legitimate user → 100 requests/min → ✅ Allowed
Abusive client → 101st request → ❌ 429 Too Many Requests
Approach 1: In-Memory (Simple, Single Server)
// Simple fixed-window rate limiter
class RateLimiter {
constructor(options = {}) {
this.windowMs = options.windowMs || 60_000; // 1 minute
this.maxRequests = options.maxRequests || 100;
this.store = new Map(); // IP → { count, resetTime }
}
middleware() {
return (req, res, next) => {
const key = req.ip || req.connection.remoteAddress;
const now = Date.now();
const record = this.store.get(key);
if (!record || now > record.resetTime) {
// New window
const newRecord = { count: 1, resetTime: now + this.windowMs };
this.store.set(key, newRecord);
return this._addHeaders(res, newRecord, next);
}
if (record.count >= this.maxRequests) {
const retryAfter = Math.ceil((record.resetTime - now) / 1000);
return res.status(429).json({
error: 'Too many requests',
retryAfter,
});
}
record.count++;
return this._addHeaders(res, record, next);
};
}
_addHeaders(res, record, next) {
res.set('X-RateLimit-Limit', this.maxRequests);
res.set('X-RateLimit-Remaining', Math.max(0, this.maxRequests - record.count));
res.set('X-RateLimit-Reset', new Date(record.resetTime).toISOString());
return next();
}
}
// Usage
app.use(new RateLimiter({ windowMs: 60_000, maxRequests: 100 }).middleware());
Approach 2: Sliding Window Log (More Accurate)
// Instead of "100 requests per minute" (fixed window),
// "no more than 100 requests in any 60-second rolling window"
class SlidingWindowLimiter {
constructor(windowMs = 60_000, maxRequests = 100) {
this.windowMs = windowMs;
this.maxRequests = maxRequests;
this.requests = new Map(); // IP → [timestamp, timestamp, ...]
}
middleware() {
return (req, res, next) => {
const key = req.ip;
const now = Date.now();
let timestamps = this.requests.get(key) || [];
// Remove old entries outside the window
timestamps = timestamps.filter(t => now - t < this.windowMs);
if (timestamps.length >= this.maxRequests) {
const oldestInWindow = timestamps[0];
const retryAfter = Math.ceil(
(this.windowMs - (now - oldestInWindow)) / 1000
);
return res.status(429).json({
error: 'Too many requests',
retryAfter,
});
}
timestamps.push(now);
this.requests.set(key, timestamps);
// Cleanup old entries periodically
if (Math.random() < 0.01) { // 1% chance per request
this._cleanup();
}
next();
};
}
_cleanup() {
const now = Date.now();
for (const [key, timestamps] of this.requests) {
const filtered = timestamps.filter(t => now - t < this.windowMs);
if (filtered.length === 0) {
this.requests.delete(key);
} else {
this.requests.set(key, filtered);
}
}
}
}
Approach 3: Redis-Based (Production Ready, Multi-Server)
npm install ioredis
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
class RedisRateLimiter {
constructor(redisClient, options = {}) {
this.redis = redisClient;
this.windowMs = options.windowMs || 60_000;
this.maxRequests = options.maxRequests || 100;
this.prefix = options.prefix || 'ratelimit:';
}
async middleware() {
return async (req, res, next) => {
const key = this.prefix + (req.ip || req.connection.remoteAddress);
const now = Date.now();
const windowStart = now - this.windowMs;
// Use sorted set for sliding window
const pipeline = this.redis.pipeline();
// Remove entries outside the window
pipeline.zremrangebyscore(key, '-inf', windowStart);
// Count remaining entries
pipeline.zcard(key);
// Add current request
pipeline.zadd(key, now, `${now}-${Math.random()}`);
// Set expiry on the key
pipeline.pexpire(key, this.windowMs);
const results = await pipeline.exec();
const count = results[1][1]; // zcard result
res.set('X-RateLimit-Limit', this.maxRequests);
res.set('X-RateLimit-Remaining', Math.max(0, this.maxRequests - count));
if (count > this.maxRequests) {
// Find when the oldest entry expires
const oldest = await this.redis.zrange(key, 0, 0, 'WITHSCORES');
const retryAfter = Math.ceil(
((oldest[0][1] + this.windowMs) - now) / 1000
);
return res.status(429).json({
error: 'Too many requests',
retryAfter: Math.max(1, retryAfter),
});
}
next();
};
}
}
Using Express Rate Limit (Recommended for Most Apps)
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
// General API limiter
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: { error: 'Too many requests, please try again later.' },
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false, // Disable X-RateLimit-* headers (use standard)
keyGenerator: (req) => req.ip,
handler: (req, res) => {
res.status(429).json({ error: 'Too many requests' });
},
});
// Apply to all routes
app.use('/api/', apiLimiter);
// Stricter limiter for auth endpoints
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // Only 5 login attempts per hour!
message: { error: 'Too many login attempts. Try again later.' },
skipSuccessfulRequests: true, // Don't count successful logins
});
app.post('/api/login', authLimiter, loginHandler);
// Very lenient for static assets
const staticLimiter = rateLimit({
windowMs: 60 * 1000,
max: 200,
});
app.use(staticLimiter);
Advanced: Tiered Rate Limiting
// Different limits based on user tier
function createTieredLimiter(tiers) {
return async (req, res, next) => {
const user = req.user; // From auth middleware
if (!user) {
// Anonymous users — strictest limit
return tiers.anonymous.middleware()(req, res, next);
}
const tierLimits = tiers[user.tier] || tiers.free;
return tierLimits.middleware()(req, res, next);
};
}
const limiter = createTieredLimiter({
anonymous: rateLimit({ windowMs: 60_000, max: 20 }),
free: rateLimit({ windowMs: 60_000, max: 50 }),
pro: rateLimit({ windowMs: 60_000, max: 500 }),
enterprise: rateLimit({ windowMs: 60_000, max: 5000 }),
});
app.use('/api/', authMiddleware, limiter);
Response Headers Explained
HTTP/1.1 200 OK
X-RateLimit-Limit: 100 # Max requests allowed
X-RateLimit-Remaining: 97 # How many left this window
X-RateLimit-Reset: 1715841200 # Unix timestamp when window resets
Retry-After: 42 # Seconds until you can try again (on 429)
How do you handle rate limiting? Any production war stories?
Follow @armorbreak for more Node.js content.
Top comments (0)