Skip to main content

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.