Express.js rate limiting ๊ตฌํ
Implementing rate limiting in Express.js with in-memory, Redis-backed, and sliding window approaches to protect APIs from abuse.
Express.js rate limiting ๊ตฌํ
Introduction
Rate limiting is essential for protecting APIs from abuse, DDoS attacks, and accidental overload. Without rate limiting, a single client can exhaust your server resources. This post covers multiple rate limiting strategies for Express.js.
Environment
node --version
# v20.11.0
npm list express express-rate-limit
# my-app@1.0.0
# +-- express@4.18.2
# +-- express-rate-limit@7.1.5Problem
Without rate limiting, the API is vulnerable:
// No rate limiting - vulnerable to abuse
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
// Attacker can try thousands of passwords per second!
const user = await authenticate(email, password);
if (user) {
res.json({ token: generateToken(user) });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});Analysis
Rate limiting strategies:
Fixed Window: Count requests per time window
Sliding Window: Count requests in rolling window
Token Bucket: Allow bursts with sustained rate
Leaky Bucket: Process requests at fixed rateThe rate limiting flow:
Request โ Check count โ Under limit? โ Allow โ Increment count
โ Over limit? โ Reject (429)Solution
Solution 1: Basic rate limiting with express-rate-limit
const rateLimit = require('express-rate-limit');
// General API rate limit
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: {
error: 'Too many requests',
retryAfter: '15 minutes'
},
standardHeaders: true,
legacyHeaders: false
});
app.use('/api/', apiLimiter);Solution 2: Different limits for different endpoints
const rateLimit = require('express-rate-limit');
// Strict limit for authentication endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 5 attempts per 15 minutes
message: {
error: 'Too many login attempts',
retryAfter: '15 minutes'
}
});
// General API limit
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
// Lenient limit for public endpoints
const publicLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 200
});
app.use('/api/auth/', authLimiter);
app.use('/api/', apiLimiter);
app.use('/api/public/', publicLimiter);Solution 3: Redis-backed rate limiting (distributed)
const RedisStore = require('rate-limit-redis');
const { createClient } = require('redis');
const redisClient = createClient({
url: 'redis://localhost:6379'
});
redisClient.connect();
const redisLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
store: new RedisStore({
sendCommand: (...args) => redisClient.sendCommand(args),
prefix: 'rl:' // Key prefix
})
});
app.use('/api/', redisLimiter);Solution 4: Sliding window rate limiting
class SlidingWindowRateLimiter {
constructor(redisClient, options = {}) {
this.redis = redisClient;
this.windowMs = options.windowMs || 60000;
this.maxRequests = options.maxRequests || 100;
}
async isAllowed(key) {
const now = Date.now();
const windowStart = now - this.windowMs;
// Remove old entries
await this.redis.zremrangebyscore(key, 0, windowStart);
// Count current requests
const count = await this.redis.zcard(key);
if (count >= this.maxRequests) {
return { allowed: false, retryAfter: this.windowMs };
}
// Add current request
await this.redis.zadd(key, now, `${now}-${Math.random()}`);
await this.redis.expire(key, Math.ceil(this.windowMs / 1000));
return {
allowed: true,
remaining: this.maxRequests - count - 1
};
}
}
// Usage
const limiter = new SlidingWindowRateLimiter(redisClient, {
windowMs: 60000, // 1 minute
maxRequests: 100 // 100 requests per minute
});
app.use(async (req, res, next) => {
const key = `rate:${req.ip}`;
const result = await limiter.isAllowed(key);
if (!result.allowed) {
return res.status(429).json({
error: 'Rate limit exceeded',
retryAfter: Math.ceil(result.retryAfter / 1000)
});
}
res.setHeader('X-RateLimit-Remaining', result.remaining);
next();
});Solution 5: Custom rate limiter with token bucket
class TokenBucketLimiter {
constructor(options = {}) {
this.buckets = new Map();
this.capacity = options.capacity || 10;
this.refillRate = options.refillRate || 1; // tokens per second
}
getToken(key) {
const now = Date.now();
if (!this.buckets.has(key)) {
this.buckets.set(key, {
tokens: this.capacity,
lastRefill: now
});
}
const bucket = this.buckets.get(key);
// Refill tokens
const elapsed = (now - bucket.lastRefill) / 1000;
bucket.tokens = Math.min(this.capacity, bucket.tokens + elapsed * this.refillRate);
bucket.lastRefill = now;
if (bucket.tokens >= 1) {
bucket.tokens -= 1;
return true;
}
return false;
}
}
const bucketLimiter = new TokenBucketLimiter({
capacity: 10,
refillRate: 1
});
app.use((req, res, next) => {
if (bucketLimiter.getToken(req.ip)) {
next();
} else {
res.status(429).json({ error: 'Rate limit exceeded' });
}
});Lessons Learned
- Use Redis for distributed rate limiting - In-memory does not work across instances
- Set different limits for different endpoints - Auth endpoints need stricter limits
- Include rate limit headers - Help clients understand their limits
- Consider sliding window - More accurate than fixed window
- Test rate limiting - Use tools like
aborwrkto verify
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.