Aller au contenu principal

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

  1. Use Redis - For distributed systems and multiple servers
  2. Set Appropriate Limits - Balance security and UX
  3. Different Limits per Endpoint - Protect expensive operations more
  4. Monitor Metrics - Track rate limit violations
  5. Clear Error Messages - Tell users when they can retry
  6. Whitelist Internal Services - Don't limit your own infrastructure
  7. Test Thoroughly - Ensure limits work as expected
  8. 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


️ Important: Always use Redis store in production for rate limiting. In-memory store doesn't work across multiple servers and resets on restart.