Rate Limiting
Rate limiting is a critical security feature that protects your API from abuse, DDoS attacks, and ensures fair resource usage. JifiJs provides flexible rate limiting capabilities out of the box.
Why Rate Limiting?
Rate limiting protects your API from:
- Brute Force Attacks - Prevent password guessing attempts
- DDoS Attacks - Mitigate denial of service
- API Abuse - Stop scrapers and excessive usage
- Resource Exhaustion - Prevent server overload
- Cost Control - Limit expensive operations
- Fair Usage - Ensure equal access for all users
️ Implementation
JifiJs uses express-rate-limit with Redis store for distributed rate limiting across multiple servers.
Basic Rate Limiting
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import { redisClient } from './config/redis';
// Global rate limiter
const limiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'rl:',
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: {
error: 'Too many requests, please try again later.',
retryAfter: 900, // seconds
},
standardHeaders: true, // Return rate limit info in RateLimit-* headers
legacyHeaders: false, // Disable X-RateLimit-* headers
});
// Apply to all routes
app.use(limiter);
Route-Specific Limits
// Strict limit for authentication endpoints
const authLimiter = rateLimit({
store: new RedisStore({ client: redisClient, prefix: 'rl:auth:' }),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
skipSuccessfulRequests: true, // Don't count successful logins
message: {
error: 'Too many login attempts. Please try again after 15 minutes.',
},
});
// Apply to specific routes
app.post('/api/auth/login', authLimiter, authController.login);
app.post('/api/auth/register', authLimiter, authController.register);
Rate Limiting Strategies
1. Fixed Window
Simplest approach - fixed time windows:
const fixedWindow = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 20, // 20 requests per minute
});
Timeline:
0:00 ───── 0:30 ───── 1:00 ───── 1:30 ───── 2:00
[──────── 20 req ────────][──────── 20 req ────────]
2. Sliding Window
More accurate, prevents burst at window edges:
import { rateLimit } from 'express-rate-limit';
const slidingWindow = rateLimit({
windowMs: 60 * 1000,
max: 20,
// Sliding window is default in express-rate-limit 6+
});
Timeline:
0:00 ───── 0:30 ───── 1:00 ───── 1:30 ───── 2:00
[──── 20 req ────]
[──── 20 req ────]
[──── 20 req ────]
3. Tiered Limits (User-Based)
Different limits for different user types:
const createUserLimiter = (tier: 'free' | 'premium' | 'enterprise') => {
const limits = {
free: { windowMs: 60 * 1000, max: 20 },
premium: { windowMs: 60 * 1000, max: 100 },
enterprise: { windowMs: 60 * 1000, max: 1000 },
};
return rateLimit({
...limits[tier],
store: new RedisStore({
client: redisClient,
prefix: `rl:${tier}:`,
}),
});
};
// Middleware to apply appropriate limiter
const adaptiveLimiter = (req, res, next) => {
const user = req.user;
if (!user) {
return createUserLimiter('free')(req, res, next);
}
const limiter = createUserLimiter(user.tier);
return limiter(req, res, next);
};
app.use('/api', adaptiveLimiter);
4. Custom Key Function
Rate limit by custom criteria:
// Rate limit by API key
const apiKeyLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
keyGenerator: (req) => {
return req.headers['x-api-key'] || req.ip;
},
});
// Rate limit by user ID
const userLimiter = rateLimit({
windowMs: 60 * 1000,
max: 50,
keyGenerator: (req) => {
return req.user?.id || req.ip;
},
skip: (req) => !req.user, // Skip for unauthenticated requests
});
️ Endpoint-Specific Strategies
Authentication Endpoints
Strict limits to prevent brute force:
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
skipSuccessfulRequests: true,
message: {
error: 'Too many login attempts. Account temporarily locked.',
lockoutTime: 15 * 60, // seconds
},
});
const passwordResetLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 3, // 3 password reset requests per hour
message: {
error: 'Too many password reset requests. Please try again later.',
},
});
app.post('/api/auth/login', loginLimiter, authController.login);
app.post('/api/auth/forgot-password', passwordResetLimiter, authController.forgotPassword);
File Upload Endpoints
Prevent abuse of expensive operations:
const uploadLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10, // 10 uploads per hour
message: {
error: 'Upload limit exceeded. Please try again in an hour.',
},
// Different limits based on file size
skip: (req) => {
const contentLength = req.headers['content-length'];
// Skip rate limit for small files
return parseInt(contentLength || '0') < 1024 * 1024; // < 1MB
},
});
app.post('/api/upload', uploadLimiter, uploadController.upload);
Search Endpoints
Protect expensive database queries:
const searchLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 10, // 10 searches per minute
message: {
error: 'Too many search requests. Please slow down.',
},
});
app.get('/api/search', searchLimiter, searchController.search);
Advanced Features
Progressive Delays
Slow down abusive requests:
import slowDown from 'express-slow-down';
const speedLimiter = slowDown({
windowMs: 15 * 60 * 1000,
delayAfter: 50, // Allow 50 requests per window without delay
delayMs: 500, // Add 500ms delay per request after 50
maxDelayMs: 20000, // Maximum delay of 20 seconds
});
app.use('/api', speedLimiter);
Skip Successful Requests
Don't penalize successful operations:
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true, // Don't count successful logins
handler: async (req, res, next, options) => {
// Log failed attempts
await AuditLog.create({
action: 'rate_limit_exceeded',
ip: req.ip,
endpoint: req.path,
});
res.status(429).json({
error: options.message,
});
},
});
Whitelist/Blacklist
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
skip: (req) => {
// Whitelist internal IPs
const trustedIPs = ['127.0.0.1', '10.0.0.0/8'];
return trustedIPs.some(ip => req.ip.startsWith(ip));
},
});
// Or block specific IPs
const blockList = new Set(['1.2.3.4', '5.6.7.8']);
const blockingLimiter = rateLimit({
windowMs: 60 * 1000,
max: 1, // Essentially block
skip: (req) => !blockList.has(req.ip),
});
Dynamic Limits
Adjust limits based on server load:
import os from 'os';
const createDynamicLimiter = () => {
return rateLimit({
windowMs: 60 * 1000,
max: async (req) => {
const load = os.loadavg()[0]; // 1-minute load average
// Reduce limits under high load
if (load > 4) return 10; // High load
if (load > 2) return 50; // Medium load
return 100; // Normal load
},
});
};
Response Headers
JifiJs includes rate limit information in response headers:
HTTP/1.1 200 OK
RateLimit-Limit: 100
RateLimit-Remaining: 95
RateLimit-Reset: 1640000000
When limit is exceeded:
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 1640000000
Retry-After: 900
{
"error": "Too many requests, please try again later.",
"retryAfter": 900
}
Monitoring
Log Rate Limit Events
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
handler: (req, res, next, options) => {
// Log to monitoring service
logger.warn('Rate limit exceeded', {
ip: req.ip,
path: req.path,
user: req.user?.id,
});
// Send alert for suspicious activity
if (req.rateLimit.current > 200) {
SecurityAlert.send({
type: 'EXCESSIVE_REQUESTS',
ip: req.ip,
count: req.rateLimit.current,
});
}
res.status(429).json({ error: options.message });
},
});
Track Metrics
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
onLimitReached: (req, res, options) => {
// Track in analytics
metrics.increment('rate_limit.exceeded', {
endpoint: req.path,
method: req.method,
});
},
});
Testing Rate Limits
Unit Tests
import request from 'supertest';
import app from './app';
describe('Rate Limiting', () => {
it('should allow requests within limit', async () => {
for (let i = 0; i < 5; i++) {
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'wrong' });
expect(response.status).not.toBe(429);
}
});
it('should block requests exceeding limit', async () => {
// Exceed limit
for (let i = 0; i < 6; i++) {
await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'wrong' });
}
// Should be rate limited
const response = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'wrong' });
expect(response.status).toBe(429);
expect(response.body.error).toContain('Too many');
});
it('should include rate limit headers', async () => {
const response = await request(app).get('/api/users');
expect(response.headers).toHaveProperty('ratelimit-limit');
expect(response.headers).toHaveProperty('ratelimit-remaining');
expect(response.headers).toHaveProperty('ratelimit-reset');
});
});
Configuration
Environment Variables
# Rate Limiting Configuration
RATE_LIMIT_WINDOW_MS=900000 # 15 minutes
RATE_LIMIT_MAX_REQUESTS=100 # Max requests per window
RATE_LIMIT_AUTH_MAX=5 # Max auth attempts
RATE_LIMIT_SKIP_SUCCESSFUL=true # Skip successful requests
Configuration File
// config/rate-limit.ts
export const rateLimitConfig = {
global: {
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'),
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100'),
},
auth: {
windowMs: 15 * 60 * 1000,
max: parseInt(process.env.RATE_LIMIT_AUTH_MAX || '5'),
skipSuccessfulRequests: true,
},
upload: {
windowMs: 60 * 60 * 1000,
max: 10,
},
search: {
windowMs: 60 * 1000,
max: 20,
},
};
Best Practices
- Use Redis - For distributed systems and multiple servers
- Set Appropriate Limits - Balance security and UX
- Different Limits per Endpoint - Protect expensive operations more
- Monitor Metrics - Track rate limit violations
- Clear Error Messages - Tell users when they can retry
- Whitelist Internal Services - Don't limit your own infrastructure
- Test Thoroughly - Ensure limits work as expected
- Log Violations - Detect attack patterns
️ Common Pitfalls
Too Strict - Blocking legitimate users Too Lenient - Not protecting against abuse Fixed Window Only - Allows burst attacks at window edges No Monitoring - Missing abuse patterns Single Global Limit - One size doesn't fit all
Related Documentation
️ Important: Always use Redis store in production for rate limiting. In-memory store doesn't work across multiple servers and resets on restart.