Rate Limiting for Security: Preventing Abuse and Brute Force Attacks
In this tutorial, you will learn about Rate Limiting for Security: Preventing Abuse and Brute Force Attacks. We cover key concepts, practical examples, and best practices to help you master this topic.
Rate limiting controls how many requests a client can make within a specific time window. As a security control, it protects against brute force login attempts, credential stuffing, API key abuse, DDoS attacks, and web scraping by limiting the rate of requests from a single source.
flowchart TB
Attacker[Attacker] -->|1000 requests/sec| RateLimiter[Rate Limiter]
RateLimiter -->|Allow 100/min| Normal[Legitimate User]
RateLimiter -->|Block 429| Blocked[Blocked Client]
RateLimiter -->|Distributed| Redis[(Redis Counter)]
subgraph Algorithms
TB[Token Bucket]
SW[Sliding Window]
FP[Fixed Window]
end
RateLimiter --> TB
RateLimiter --> SW
RateLimiter --> FP
What You'll Learn
- Rate limiting algorithms for security (token bucket, Sliding Window, fixed window)
- Per-IP, per-user, and per-endpoint rate limiting
- Brute force protection with rate limiting
- Distributed rate limiting with Redis
Why It Matters
Without rate limiting, a single attacker can try millions of passwords against your login endpoint in minutes. Rate limiting makes brute force attacks economically infeasible by limiting attempts to a few per minute.
Real-World Use
A payment API limits each API key to 1000 requests per hour. A compromised API key can only cause limited damage before rate limiting kicks in. Additionally, the login endpoint is limited to 5 attempts per IP per 15 minutes, preventing credential stuffing attacks.
Rate Limiting for Security
Login Rate Limiting (Brute Force Protection)
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: { error: 'Too many login attempts. Please try again later.' },
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => {
// Rate limit by IP + username to prevent distributed brute force
return `${req.ip}:${req.body.username || 'unknown'}`;
},
handler: (req, res) => {
// Log the rate limit event for security monitoring
console.warn(`Rate limit hit: login from ${req.ip} for user ${req.body.username}`);
res.status(429).json({ error: 'Too many login attempts.' });
}
});
app.post('/api/login', loginLimiter, async (req, res) => {
// Login logic
});
Expected output:
After 5 failed login attempts for the same user from any IP (or same IP for any user), subsequent attempts return 429.
API Key Rate Limiting
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 1000,
keyGenerator: (req) => req.headers['x-api-key'] || req.ip,
handler: (req, res) => {
res.status(429).json({
error: 'Rate limit exceeded',
retryAfter: res.getHeader('Retry-After')
});
}
});
const strictEndpointLimiter = rateLimit({
windowMs: 60 * 1000,
max: 30,
keyGenerator: (req) => req.headers['x-api-key'] || req.ip
});
app.use('/api/', apiLimiter);
app.use('/api/search', strictEndpointLimiter);
Expected output:
1000 requests per API key per hour globally. 30 requests per minute for the search endpoint specifically.
Distributed Rate Limiting with Redis (Sliding Window)
const redis = require('redis');
const client = redis.createClient();
async function slidingWindowRateLimit(key, maxRequests, windowSeconds) {
const now = Date.now();
const windowStart = now - windowSeconds * 1000;
const multi = client.multi();
multi.zRemRangeByScore(key, 0, windowStart);
multi.zCard(key);
multi.zAdd(key, now, `${now}-${Math.random()}`);
multi.expire(key, windowSeconds);
const [, count] = await multi.exec();
if (count >= maxRequests) {
return { allowed: false, remaining: 0 };
}
return { allowed: true, remaining: maxRequests - count - 1 };
}
// Middleware
async function redisRateLimit(req, res, next) {
const key = `ratelimit:${req.ip}:${req.path}`;
const result = await slidingWindowRateLimit(key, 10, 60);
res.set('X-RateLimit-Remaining', result.remaining);
if (!result.allowed) {
return res.status(429).json({ error: 'Rate limit exceeded' });
}
next();
}
Expected output:
Sliding window tracks request timestamps in Redis sorted set. Old entries are removed, count is precise. Limits to 10 requests per minute per IP per path.
Common Mistakes
- Rate limiting only by IP — attackers using botnets have many IPs. Combine IP with user ID or API key.
- Not rate limiting expensive endpoints differently — search and export endpoints should have stricter limits than simple reads.
- Using fixed window instead of sliding window — fixed window allows burst at window boundaries (e.g., 100 requests in the last second of a window).
- Not returning Retry-After header — clients need to know when they can retry.
- Rate limiting before authentication — unauthenticated users should have stricter limits than authenticated ones.
Practice Questions
- How does rate limiting prevent brute force attacks?
- What is the difference between fixed window and sliding window rate limiting?
- Why should you rate limit by more than just IP address?
- How does the Retry-After header help clients?
- What is the token bucket algorithm?
Challenge
Design a rate limiting Strategy for a social media API. Implement: (1) global rate limit per API key (1000/hour), (2) login rate limit (5/15min per IP+user), (3) post creation limit (10/hour per user), (4) search endpoint limit (30/min per key). Use Redis for distributed counting.
FAQ
Mini Project
Build a rate-limited API with three tiers: anonymous (10 req/min), authenticated (100 req/min), and premium (1000 req/min). Implement Redis-based sliding window. Add rate limit headers to all responses. Write a load test that verifies each tier's limits.
What's Next
Continue to Input Validation to learn comprehensive input validation and sanitization strategies.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro