Back to Blog
Node.js7 min read

Rate Limiting in Node.js with Redis & Express

Building robust APIs requires careful rate limiting. Learn how to implement a token-bucket rate limiter for your Node.js applications using Redis and Express middleware to prevent abuse and ensure fair usage.

Jay Salot

Jay Salot

Senior Full Stack AI Engineer

May 27, 2026 · 7 min read

Share
Coding workspace with multiple screens

API rate limiting is one of those things you don't think about until a single client floods your server and everything falls over. I hit exactly that in a recent project: we needed to handle a surge of requests without crashing our Node.js server. This post walks through how we built a custom rate limiter using Redis and Express middleware.

The Problem: Rate Limiting 101

Rate limiting is essential for protecting your API from abuse, whether it's accidental (a buggy client flooding your server) or malicious (a DDoS attack). Without it, your server can easily get overwhelmed, leading to performance degradation or even a complete outage. Think of it as controlling how many requests each client can make in a given window to keep things smooth for everyone.

Why Not Just Ignore It?

Ignoring rate limiting can have serious consequences. I've seen firsthand what happens when an API endpoint gets hammered with requests – database connection pool exhaustion, increased latency, and ultimately, unhappy users. It's much better to be proactive than reactive in these situations.

Common Rate Limiting Strategies

There are several ways to implement rate limiting. Some common strategies include:

  • Token Bucket: Each user gets a 'bucket' of tokens, representing the number of requests they can make. Each request consumes a token. Tokens are refilled at a certain rate.
  • Leaky Bucket: Similar to the token bucket, but requests are processed at a constant rate, 'leaking' out of the bucket.
  • Fixed Window: Track the number of requests within a fixed time window (e.g., 1 minute). Reset the counter at the end of the window.
  • Sliding Window: An improvement over the fixed window, it calculates the rate based on a rolling time window. This avoids the issue of bursts at the edge of fixed windows.

For our rate limiter, we opted for a combination of the token bucket and fixed window approaches, tuned to our specific application needs.

Building Our Rate Limiting Middleware

We decided to create a custom Express middleware to handle the rate limiting logic. This gave us maximum flexibility and control over the implementation. The core idea is to use Redis to store the number of requests made by each user within a specific time window.

Setting Up Redis

First, you'll need a Redis instance. You can either run it locally, use a managed service like AWS ElastiCache, or GCP Memorystore. I personally prefer using Docker for local development:

docker run -d -p 6379:6379 redis:latest

Next, install the redis package in your Node.js project:

npm install redis

The Middleware Code

Here's the TypeScript code for our rate limiting middleware:

import { Request, Response, NextFunction } from 'express';
import Redis from 'ioredis';

const redis = new Redis(); // Connect to Redis

interface RateLimitOptions {
  windowMs: number; // Time window in milliseconds
  max: number;      // Max number of requests allowed per window
  keyGenerator?: (req: Request) => string; // Function to generate the Redis key (defaults to IP address)
  message?: string; // Custom error message
}

const rateLimit = (options: RateLimitOptions) => {
  const { windowMs, max, keyGenerator = (req: Request) => req.ip, message = 'Too many requests, please try again later.' } = options;

  return async (req: Request, res: Response, next: NextFunction) => {
    const key = keyGenerator(req);

    try {
      // Increment the request count in Redis
      const current = await redis.incr(key);

      // Set the expiration time for the key if it's the first request
      if (current === 1) {
        await redis.pexpire(key, windowMs);
      }

      // Check if the request count exceeds the limit
      if (current > max) {
        res.status(429).send(message);
        return;
      }

      // Attach rate limit headers
      res.setHeader('X-RateLimit-Limit', max);
      res.setHeader('X-RateLimit-Remaining', max - current);
      res.setHeader('X-RateLimit-Reset', Date.now() + windowMs);

      next();
    } catch (error) {
      console.error('Redis error:', error);
      res.status(500).send('Server error.');
    }
  };
};

export default rateLimit;

Understanding the Code

Let's break down what this code does:

  • It connects to Redis using the ioredis library (I prefer it over the standard redis package for its promise-based API).
  • It takes an options object to configure the rate limiting behavior:
    • windowMs: The time window in milliseconds.
    • max: The maximum number of requests allowed within the window.
    • keyGenerator: A function that generates the Redis key. By default, it uses the client's IP address, but you can customize it to use user IDs or other identifiers.
    • message: A custom error message to send when the rate limit is exceeded.
  • For each request, it increments the request count in Redis using the INCR command.
  • If it's the first request within the window, it sets an expiration time on the key using the PEXPIRE command. This ensures that the request count is automatically reset after the window expires.
  • If the request count exceeds the max limit, it sends a 429 Too Many Requests error.
  • It adds rate limit headers to the response, providing information about the limit, remaining requests, and reset time.
  • It includes basic error handling to catch any Redis errors.

Using the Middleware in Express

Using the middleware is straightforward. Here's an example:

import express from 'express';
import rateLimit from './rateLimit';

const app = express();
const port = 3000;

// Apply rate limiting to all routes
app.use(rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 100,           // 100 requests per minute
  message: 'Too many requests from this IP, please try again after 1 minute.'
}));

// Define a route
app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

Applying to Specific Routes

You can also apply the middleware to specific routes:

app.get('/api/expensive', rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 10,            // 10 requests per minute
  message: 'Too many requests to this endpoint, please try again after 1 minute.'
}), (req, res) => {
  // ... expensive operation ...
  res.send('Expensive operation completed.');
});

Customizing the Key Generator

The default key generator uses the client's IP address, which is fine for basic rate limiting. However, in many cases, you'll want to use a more specific identifier, such as a user ID. Here's how you can customize the keyGenerator function:

app.use(rateLimit({
  windowMs: 60 * 1000,
  max: 100,
  keyGenerator: (req: Request) => {
    // Assuming you have user authentication middleware
    if (req.user && req.user.id) {
      return `user:${req.user.id}`;
    } else {
      return req.ip; // Fallback to IP address if user is not authenticated
    }
  },
  message: 'Too many requests, please try again later.'
}));

Gotcha: Make sure your authentication middleware runs *before* the rate limiting middleware, so req.user is populated.

Advanced Considerations

IP Address Considerations

Relying solely on IP addresses for rate limiting can be problematic. Users behind a NAT (Network Address Translation) share the same public IP address, so you might end up unfairly rate-limiting legitimate users. Consider using a more granular identifier, like a user ID, whenever possible.

Distributed Environments

In a distributed environment with multiple servers, you need to ensure that the rate limiting logic is synchronized across all instances. Using a centralized data store like Redis is crucial for achieving this. Without it, each server would have its own independent rate limit counter, effectively bypassing the intended restrictions.

Monitoring and Alerting

It's essential to monitor your rate limiting system and set up alerts for unusual activity. Track metrics like the number of requests being rate-limited, the average response time, and error rates. This will help you identify potential issues and adjust your rate limits as needed. I recommend using tools like Prometheus and Grafana for monitoring, and setting up alerts in CloudWatch or Google Cloud Monitoring.

Conclusion

Rate limiting is a crucial aspect of building robust and scalable APIs. By implementing a custom rate limiter with Redis and middleware, you can effectively protect your application from abuse and ensure fair usage. We covered the basics of rate limiting, walked through a practical example of building a custom middleware, and discussed some advanced considerations for distributed environments. Rate limiting comes down to prioritizing and controlling access to your resources. Don't wait until your server is overloaded – implement rate limiting early and often! Key takeaways: Redis is your friend, customize the key generator, and monitor everything.

#Node.js#Express#Rate Limiting#Redis#Middleware
Share

Related Articles