Rate Limiting Server: Throttle API Requests
Learn: Rate Limiting Server: Throttle API Requests
Welcome to TopperBlog! 👋
I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.
🎯 What I Write About:
• AI/ML Engineering & LLMs
• Web3 & Blockchain Development
• System Design & Architecture
• Interview Preparation (FAANG)
• Freelancing & Remote Work
• Modern Tech Stacks (Next.js, React, Rust, TypeScript)
• Performance Optimization & Best Practices
💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.
📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.
🌐 Let's connect and grow together in this amazing tech journey!
#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering
Rate Limiting Server: Throttle API Requests
Problem
APIs are vulnerable to abuse through excessive requests that can:
- Exhaust server resources and cause denial-of-service (DoS)
- Degrade performance for legitimate users
- Increase infrastructure costs
- Enable credential stuffing, brute force attacks, and scraping
- Violate service terms and SLAs
Without rate limiting, a single malicious actor can overwhelm your entire system.
Solution
Implement a multi-layered rate limiting strategy:
- Token Bucket Algorithm: Allows burst traffic while maintaining average rate limits
- Sliding Window Counters: Tracks requests in real-time windows
- Distributed Rate Limiting: Uses Redis for consistency across multiple servers
- Tiered Limits: Different limits for different user tiers (free, premium, enterprise)
- Adaptive Throttling: Adjusts limits based on system load
- Graceful Degradation: Returns informative error responses with retry information
Code Implementation
1. Basic Token Bucket Rate Limiter (Node.js/Express)
// rateLimiter.js
class TokenBucket {
constructor(capacity, refillRate) {
this.capacity = capacity;
this.tokens = capacity;
this.refillRate = refillRate; // tokens per second
this.lastRefillTime = Date.now();
}
refill() {
const now = Date.now();
const timePassed = (now - this.lastRefillTime) / 1000;
const tokensToAdd = timePassed * this.refillRate;
this.tokens = Math.min(
this.capacity,
this.tokens + tokensToAdd
);
this.lastRefillTime = now;
}
tryConsume(tokens = 1) {
this.refill();
if (this.tokens >= tokens) {
this.tokens -= tokens;
return true;
}
return false;
}
getTokensRemaining() {
this.refill();
return Math.floor(this.tokens);
}
}
module.exports = TokenBucket;
2. Redis-Based Distributed Rate Limiter
// redisRateLimiter.js
const redis = require('redis');
class RedisRateLimiter {
constructor(redisClient, options = {}) {
this.client = redisClient;
this.defaultLimit = options.limit || 100;
this.defaultWindow = options.window || 60; // seconds
}
async isAllowed(identifier, limit = this.defaultLimit, window = this.defaultWindow) {
const key = `rate_limit:${identifier}`;
const now = Date.now();
const windowStart = now - (window * 1000);
try {
// Remove old entries outside the window
await this.client.zremrangebyscore(key, 0, windowStart);
// Count requests in current window
const requestCount = await this.client.zcard(key);
if (requestCount < limit) {
// Add current request
await this.client.zadd(key, now, `${now}-${Math.random()}`);
// Set expiration
await this.client.expire(key, window + 1);
return {
allowed: true,
remaining: limit - requestCount - 1,
resetTime: Math.ceil((windowStart + window * 1000) / 1000)
};
}
// Get oldest request time for retry-after
const oldest = await this.client.zrange(key, 0, 0, { WITHSCORES: true });
const retryAfter = oldest.length > 0
? Math.ceil((oldest[0].score + window * 1000 - now) / 1000)
: window;
return {
allowed: false,
remaining: 0,
retryAfter,
resetTime: Math.ceil((oldest[0].score + window * 1000) / 1000)
};
} catch (error) {
console.error('Rate limiter error:', error);
// Fail open - allow request if Redis fails
return { allowed: true, remaining: limit };
}
}
}
module.exports = RedisRateLimiter;
3. Express Middleware Implementation
// middleware/rateLimitMiddleware.js
const RedisRateLimiter = require('../redisRateLimiter');
const redis = require('redis');
const redisClient = redis.createClient({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379
});
const limiter = new RedisRateLimiter(redisClient, {
limit: 100,
window: 60
});
// User tier configurations
const tierLimits = {
free: { limit: 10, window: 60 },
premium: { limit: 100, window: 60 },
enterprise: { limit: 10000, window: 60 }
};
const rateLimitMiddleware = async (req, res, next) => {
try {
// Identify user - use IP for anonymous, user ID for authenticated
const identifier = req.user?.id || req.ip;
const userTier = req.user?.tier || 'free';
const { limit, window } = tierLimits[userTier];
const result = await limiter.isAllowed(identifier, limit, window);
// Set rate limit headers
res.set({
'X-RateLimit-Limit': limit,
'X-RateLimit-Remaining': Math.max(0, result.remaining),
'X-RateLimit-Reset': result.resetTime
});
if (!result.allowed) {
res.set('Retry-After', result.retryAfter);
return res.status(429).json({
error: 'Too Many Requests',
message: `Rate limit exceeded. Retry after ${result.retryAfter} seconds.`,
retryAfter: result.retryAfter,
resetTime: new Date(result.resetTime * 1000).toISOString()
});
}
next();
} catch (error) {
console.error('Rate limit middleware error:', error);
next(); // Fail open
}
};
module.exports = rateLimitMiddleware;
4. Endpoint-Specific Rate Limiting
// middleware/endpointRateLimits.js
const RedisRateLimiter = require('../redisRateLimiter');
const redis = require('redis');
const redisClient = redis.createClient();
const limiter = new RedisRateLimiter(redisClient);
// Endpoint-specific configurations
const endpointLimits = {
'/api/auth/login': { limit: 5, window: 300 }, // 5 per 5 minutes
'/api/auth/register': { limit: 3, window: 3600 }, // 3 per hour
'/api/search': { limit: 30, window: 60 }, // 30 per minute
'/api/export': { limit: 10, window: 3600 }, // 10 per hour
'/api/upload': { limit: 5, window: 3600 } // 5 per hour
};
const endpointRateLimitMiddleware = async (req, res, next) => {
const endpoint = req.route?.path || req.path;
const config = endpointLimits[endpoint];
if (!config) {
return next();
}
const identifier = `${req.user?.id || req.ip}:${endpoint}`;
const result = await limiter.isAllowed(
identifier,
config.limit,
config.window
);
res.set({
'X-RateLimit-Limit': config.limit,
'X-RateLimit-Remaining': Math.max(0, result.remaining),
'X-RateLimit-Reset': result.resetTime
});
if (!result.allowed) {
res.set('Retry-After', result.retryAfter);
return res.status(429).json({
error: 'Too Many Requests',
message: `Too many requests to ${endpoint}`,
retryAfter: result.retryAfter
});
}
next();
};
module.exports = endpointRateLimitMiddleware;
5. Express Server Setup
// server.js
const express = require('express');
const rateLimitMiddleware = require('./middleware/rateLimitMiddleware');
const endpointRateLimitMiddleware = require('./middleware/endpointRateLimits');
const app = express();
// Global rate limiting
app.use(rateLimitMiddleware);
// Endpoint-specific rate limiting
app.use(endpointRateLimitMiddleware);
// Routes
app.post('/api/auth/login', (req, res) => {
res.json({ message: 'Login successful' });
});
app.post('/api/auth/register', (req, res) => {
res.json({ message: 'Registration successful' });
});
app.get('/api/search', (req, res) => {
res.json({ results: [] });
});
app.get('/api/export', (req, res) => {
res.json({ data: 'exported' });
});
app.post('/api/upload', (req, res) => {
res.json({ message: 'Upload successful' });
});
// Error handler
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
6. Advanced: Sliding Window with Quota Management
// advancedRateLimiter.js
class AdvancedRateLimiter {
constructor(redisClient) {
this.client = redisClient;
}
async checkRateLimit(userId, endpoint, config) {
const key = `quota:${userId}:${endpoint}`;
const now = Date.now();
const windowStart = now - (config.window * 1000);
// Lua script for atomic operation
const script = `
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window_start = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local window = tonumber(ARGV[4])
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now .. '-' .. math.random())
redis.call('EXPIRE', key, window + 1)
return {1, limit - count - 1, window_start + window * 1000}
else
local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
local retry_after = math.ceil((oldest[2] + window * 1000 - now) / 1000)
return {0, 0, retry_after}
end
`;
try {
const result = await this.client.eval(
script,
1,
key,
now,
windowStart,
config.limit,
config.window
);
return {
allowed: result[0] === 1,
remaining: result[1],
resetTime: result[2]
};
} catch (error) {
console.error('Rate limit check failed:', error);
return { allowed: true, remaining: config.limit };
}
}
async getQuotaStatus(userId) {
const pattern = `quota:${userId}:*`;
const keys = await this.client.keys(pattern);
const status = {};
for (const key of keys) {
const endpoint = key.split(':')[2];
const count = await this.client.zcard(key);
status[endpoint] = count;
}
return status;
}
async resetQuota(userId, endpoint) {
const key = `quota:${userId}:${endpoint}`;
await this.client.del(key);
return { message: 'Quota reset' };
}
}
module.exports = AdvancedRateLimiter;
7. Client-Side Retry Logic
// client/apiClient.js
class APIClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.maxRetries = 3;
}
async request(method, endpoint, data = null) {
let retries = 0;
while (retries < this.maxRetries) {
try {
const response = await fetch(`${this.baseURL}${endpoint}`, {
method,
headers: { 'Content-Type': 'application/json' },
body: data ? JSON.stringify(data) : null
});
// Handle rate limiting
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After')) || 60;
console.warn(`Rate limited. Retrying after ${retryAfter}s`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
retries++;
continue;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`Request failed: ${error.message}`);
throw error;
}
}
throw new Error('Max retries exceeded');
}
get(endpoint) {
return this.request('GET', endpoint);
}
post(endpoint, data) {
return this.request('POST', endpoint, data);
}
}
// Usage
const client = new APIClient('https://api.example.com');
client.post('/api/auth/login', { email: 'user@example.com', password: '***' })
.then(response => console.log('Login successful:', response))
.catch(error => console.error('Login failed:', error));
Key Features
✅ Token Bucket Algorithm - Smooth burst handling
✅ Redis Integration - Distributed, scalable rate limiting
✅ Tiered Limits - Different rates for user tiers
✅ Endpoint-Specific Rules - Granular control
✅ Informative Headers - RFC 6585 compliant responses
✅ Retry-After Support - Client guidance
✅ Fail-Open Design - Graceful degradation
✅ Atomic Operations - Lua scripts prevent race conditions
✅ Client Retry Logic - Automatic backoff
This implementation prevents abuse while maintaining excellent user experience for legitimate traffic.