How to Implement Rate Limiting in Node.js

Rate limiting is a crucial security measure that controls the rate of traffic sent by a client to an API, protecting your application from abuse, brute-force attacks, and Denial of Service (DoS) attempts. This article provides a straightforward guide on how to implement rate limiting in a Node.js and Express application, covering basic memory-based limiting and scaling to production-ready Redis setups.

Why Implement Rate Limiting?

Without rate limiting, malicious users or malfunctioning scripts can flood your API with thousands of requests per second. This can degrade performance for legitimate users, increase hosting costs, or crash your database. Implementing a rate limiter ensures that each user or IP address is restricted to a reasonable number of requests within a specific timeframe.

1. Basic Rate Limiting with express-rate-limit

For most Node.js applications built with Express, the easiest way to implement rate limiting is by using the express-rate-limit package.

First, install the package:

npm install express-rate-limit

Next, integrate the middleware into your Express application:

const express = require('express');
const rateLimit = require('express-rate-limit');

const app = express();

// Define the rate limiting rule
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
  message: 'Too many requests from this IP, please try again after 15 minutes',
  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
  legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});

// Apply the rate limiting middleware to all requests
app.use('/api/', apiLimiter);

app.get('/api/data', (req, res) => {
  res.send('This is secure API data.');
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

2. Key Configuration Options

The express-rate-limit library offers several options to customize your rate-limiting strategy:

3. Scaling to Production with Redis

By default, express-rate-limit stores request counts in memory. If your Node.js application is load-balanced across multiple servers or containerized in Docker, the in-memory store will not work effectively because each server instance will track requests independently.

To solve this, use an external data store like Redis to track request counts globally.

First, install the Redis store adapter and the Redis client:

npm install rate-limit-redis redis

Then, configure the rate limiter to use the Redis backend:

const express = require('express');
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis').default;
const { createClient } = require('redis');

const app = express();

// Create a Redis client
const redisClient = createClient({ url: 'redis://localhost:6379' });
redisClient.connect().catch(console.error);

const redisLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
  // Use Redis to store the request state
  store: new RedisStore({
    sendCommand: (...args) => redisClient.sendCommand(args),
  }),
  message: 'Too many requests, please try again later.',
  standardHeaders: true,
  legacyHeaders: false,
});

app.use('/api/', redisLimiter);

Using this Redis configuration ensures that no matter how many API instances you run, the rate limits are applied consistently across your entire infrastructure.