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.