Rate limiting and throttling are crucial techniques for protecting your Node.js API from abuse and ensuring fair usage. Let’s dive into how to implement these effectively.
First things first, we need to understand the difference between rate limiting and throttling. Rate limiting puts a cap on the number of requests a client can make within a specific time frame. Throttling, on the other hand, controls the rate at which requests are processed.
Now, let’s get our hands dirty with some code. We’ll start with a simple rate limiter using the popular Express.js framework and the ‘express-rate-limit’ package.
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use(limiter);
app.get('/', (req, res) => {
res.send('Hello, World!');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
In this example, we’re limiting each IP to 100 requests every 15 minutes. If a client exceeds this limit, they’ll receive a 429 Too Many Requests response.
But what if we want different limits for different routes? No problem! We can create multiple rate limiters:
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: 'Too many login attempts, please try again later.'
});
app.post('/login', authLimiter, (req, res) => {
// Login logic here
});
This setup applies a stricter limit to the login route, helping to prevent brute-force attacks.
Now, let’s talk about throttling. While the ‘express-rate-limit’ package is great for rate limiting, it doesn’t provide throttling out of the box. For throttling, we can use the ‘bottleneck’ package.
const Bottleneck = require('bottleneck');
const limiter = new Bottleneck({
minTime: 100 // Ensure a minimum of 100ms between each request
});
app.get('/api', async (req, res) => {
try {
const result = await limiter.schedule(() => {
// Your API logic here
return 'API response';
});
res.send(result);
} catch (err) {
res.status(500).send('Error occurred');
}
});
This setup ensures that there’s at least a 100ms gap between each request processed by the ‘/api’ route.
But what if we’re dealing with a distributed system? In that case, we need a centralized way to track request counts. Redis is a popular choice for this. Let’s see how we can use Redis with ‘express-rate-limit’:
const redis = require('redis');
const { RateLimiterRedis } = require('rate-limiter-flexible');
const redisClient = redis.createClient({
host: 'localhost',
port: 6379,
});
const rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
points: 10, // Number of points
duration: 1, // Per second
});
app.use((req, res, next) => {
rateLimiter.consume(req.ip)
.then(() => {
next();
})
.catch(() => {
res.status(429).send('Too Many Requests');
});
});
This setup uses Redis to store the request counts, allowing multiple Node.js instances to share the same rate limiting data.
Now, let’s talk about some best practices. It’s always a good idea to inform your API users about their rate limit status. You can do this by including rate limit information in your response headers:
app.use((req, res, next) => {
res.on('finish', () => {
res.setHeader('X-RateLimit-Limit', 100);
res.setHeader('X-RateLimit-Remaining', 100 - req.rateLimit.current);
res.setHeader('X-RateLimit-Reset', new Date(req.rateLimit.resetTime).getTime() / 1000);
});
next();
});
These headers give clients information about their current rate limit status, helping them to manage their request rate.
Another important consideration is handling burst traffic. Sometimes, you might want to allow a certain number of requests to go through even if they exceed the rate limit. The ‘token bucket’ algorithm is perfect for this. Let’s implement it:
class TokenBucket {
constructor(capacity, fillPerSecond) {
this.capacity = capacity;
this.fillPerSecond = fillPerSecond;
this.tokens = capacity;
this.lastFilled = Date.now();
}
take() {
this.refill();
if (this.tokens > 0) {
this.tokens -= 1;
return true;
}
return false;
}
refill() {
const now = Date.now();
const delta = (now - this.lastFilled) / 1000;
this.tokens = Math.min(this.capacity, this.tokens + delta * this.fillPerSecond);
this.lastFilled = now;
}
}
const bucket = new TokenBucket(5, 1); // 5 tokens, refills at 1 token per second
app.use((req, res, next) => {
if (bucket.take()) {
next();
} else {
res.status(429).send('Too Many Requests');
}
});
This implementation allows for burst traffic (up to the bucket capacity) while still maintaining an average rate limit.
Now, let’s consider the case where you want to apply different rate limits to different user tiers. We can achieve this by using a middleware that checks the user’s tier and applies the appropriate limit:
const createRateLimiter = (points, duration) => {
return new RateLimiterRedis({
storeClient: redisClient,
points,
duration,
});
};
const rateLimiters = {
free: createRateLimiter(10, 60),
premium: createRateLimiter(50, 60),
enterprise: createRateLimiter(100, 60),
};
app.use(async (req, res, next) => {
const userTier = await getUserTier(req.user.id); // Implement this function
const limiter = rateLimiters[userTier] || rateLimiters.free;
try {
await limiter.consume(req.ip);
next();
} catch {
res.status(429).send('Too Many Requests');
}
});
This setup allows you to easily manage different rate limits for different user tiers.
Let’s not forget about websockets. If your API uses websockets, you’ll need to implement rate limiting for those connections too. Here’s a simple example using the ‘ws’ package:
const WebSocket = require('ws');
const Bottleneck = require('bottleneck');
const wss = new WebSocket.Server({ port: 8080 });
const limiter = new Bottleneck({
reservoir: 5, // initial value
reservoirRefreshAmount: 5,
reservoirRefreshInterval: 1000, // must be divisible by 250
maxConcurrent: 1,
minTime: 200
});
wss.on('connection', (ws) => {
ws.on('message', async (message) => {
try {
await limiter.schedule(() => {
// Process the message
console.log('Received: %s', message);
});
} catch (error) {
ws.send('Rate limit exceeded');
}
});
});
This setup limits each websocket connection to 5 messages per second, with a minimum of 200ms between each message.
Now, let’s talk about monitoring and logging. It’s crucial to keep track of your rate limiting and throttling in action. You can use a logging library like Winston to log rate limit hits:
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
defaultMeta: { service: 'rate-limiter' },
transports: [
new winston.transports.File({ filename: 'rate-limiter.log' }),
],
});
app.use((req, res, next) => {
rateLimiter.consume(req.ip)
.then(() => {
next();
})
.catch(() => {
logger.warn(`Rate limit exceeded for IP: ${req.ip}`);
res.status(429).send('Too Many Requests');
});
});
This setup logs every rate limit hit, allowing you to monitor for potential abuse or capacity issues.
Lastly, let’s consider how to handle rate limiting in a microservices architecture. In this case, you might want to implement rate limiting at the API gateway level. Here’s a simple example using Express Gateway:
const gateway = require('express-gateway');
gateway()
.load(require('path-to-your-config'))
.run();
// In your gateway config file:
policies:
- rate-limit:
- action:
name: 'rate-limit'
max: 10
windowMs: 60000
rateLimitBy: 'ip'
This setup applies rate limiting at the gateway level, protecting all your microservices with a single configuration.
In conclusion, implementing rate limiting and throttling in your Node.js API is crucial for maintaining performance and preventing abuse. By using libraries like ‘express-rate-limit’ and ‘bottleneck’, and considering factors like distributed systems, user tiers, and websockets, you can create a robust system that ensures fair usage of your API. Remember to monitor your rate limiting system and adjust as needed based on your application’s specific requirements and traffic patterns. Happy coding!