Skip to content

Express Rate Limit Middleware — Node.js API Throttling with express-rate-limit

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Express Rate Limit Middleware. We cover key concepts, practical examples, and best practices to help you master this topic.

The express-rate-limit middleware provides simple yet powerful rate limiting for Express.js applications with built-in memory store, custom store support (Redis, MongoDB), and configurable response handling.

What You'll Learn

  • How to install and configure express-rate-limit
  • How to use Redis as a distributed rate limit store
  • How to create custom skip and key generation functions

Why It Matters

Express is one of the most popular Node.js frameworks. Adding rate limiting via middleware is the simplest way to protect your API. Express-rate-limit handles the complexity of window tracking, header generation, and response formatting out of the box.

Real-World Use

DodaTech's Node.js API uses express-rate-limit with three configurations: a global limiter (100 req/min per IP), an auth endpoint limiter (5 req/min per IP using keyGenerator), and a per-user limiter that uses the authenticated user ID from the JWT token.

flowchart LR
    A["Request"] --> B["Global Rate\nLimiter"]
    B --> C{"Auth\nEndpoint?"}
    C -->|"Yes"| D["Auth Rate\nLimiter (5/min)"]
    C -->|"No"| E["Standard Rate\nLimiter (100/min)"]
    D --> F{"Within\nlimit?"}
    E --> F
    F -->|"Yes"| G["Route Handler"]
    F -->|"No"| H["429 Response"]
    style B fill:#dbeafe,stroke:#2563eb
    style D fill:#fef3c7,stroke:#d97706
    style H fill:#fecaca,stroke:#dc2626

Basic Configuration

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

const app = express();

// Global rate limiter
const globalLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute window
  max: 100, // 100 requests per minute
  standardHeaders: true, // Return rate limit info in headers
  legacyHeaders: false, // Disable X-RateLimit-* headers
  message: {
    error: 'rate_limit_exceeded',
    message: 'Too many requests, please try again later.',
    retryAfter: '60 seconds'
  }
});

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

Per-Endpoint Rate Limits

// Strict limiter for auth endpoints
const authLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 5,
  message: {
    error: 'auth_rate_limit',
    message: 'Too many authentication attempts.',
    retryAfter: 60
  },
  keyGenerator: (req) => {
    // Use IP + username for auth rate limiting
    return req.ip + ':' + (req.body.username || '');
  },
  handler: (req, res) => {
    res.status(429).json({
      error: 'auth_rate_limit',
      message: 'Too many login attempts. Account temporarily locked.',
      retryAfter: 60,
      lockoutDuration: '5 minutes'
    });
  }
});

app.use('/api/auth/login', authLimiter);

// Generous limiter for read-only endpoints
const readLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 500,
  message: {
    error: 'rate_limit_exceeded',
    message: 'Read limit exceeded.'
  }
});

app.use('/api/public/', readLimiter);

Redis Store Configuration

const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');

const redisClient = new Redis({
  host: 'redis-cluster.dodatech.com',
  port: 6379,
  enableOfflineQueue: false
});

const distributedLimiter = rateLimit({
  store: new RedisStore({
    sendCommand: (...args) => redisClient.call(...args),
    prefix: 'rl:express:'
  }),
  windowMs: 60 * 1000,
  max: 100,
  standardHeaders: true,
  // Skip rate limiting for health checks
  skip: (req) => req.path === '/health'
});

app.use(distributedLimiter);

// Health check endpoint (not rate limited)
app.get('/health', (req, res) => {
  res.json({ status: 'healthy' });
});

Custom Key Generator

// Rate limit by authenticated user ID
const userRateLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 100,
  keyGenerator: (req) => {
    // Use JWT user ID if authenticated, fall back to IP
    if (req.user && req.user.id) {
      return `user:${req.user.id}`;
    }
    return `ip:${req.ip}`;
  },
  skipFailedRequests: false,
  requestWasSuccessful: (req, res) => {
    return res.statusCode < 400;
  }
});

Common Mistakes

1. Using Default Memory Store in Production

The default memory store does not scale across multiple server instances. Use Redis or another shared store.

2. Not Setting standardHeaders

Without standard headers, clients cannot determine remaining limits or retry times. Enable standardHeaders for better client experience.

3. Forgetting to Skip Health Checks

Health check endpoints should be excluded from rate limiting to prevent false monitoring alerts.

4. Using the Same Limiter for All Routes

Different endpoints need different limits. Create separate limiters for auth, write, and read endpoints.

5. Not Handling Rate Limit Errors Gracefully

Customize the error response to include helpful information like retry timing and which limit was exceeded.

Practice Questions

  1. What is the default store for express-rate-limit?
  2. How do you configure per-endpoint rate limits?
  3. What is a keyGenerator function used for?
  4. Why use Redis as a store instead of the default memory store?
  5. How do you skip rate limiting for specific requests?

Answers

  1. In-memory store (MemoryStore). 2. Create separate rateLimit instances and apply them to different routes. 3. To customize the key used for rate limit counting (e.g., by user ID). 4. For distributed rate limiting across multiple server instances. 5. Use the skip option with a function that returns true for requests to skip.

Challenge

Build an Express.js API with three different rate limiters: global (100 req/min), auth (5 req/min with username-based key), and per-user (1000 req/min based on JWT). Use Redis for distributed storage. Include a test script that verifies each limiter.

FAQ

What is express-rate-limit?

A middleware for Express.js that provides rate limiting with configurable windows, limits, and stores.

Does express-rate-limit support distributed rate limiting?

Yes, with external stores like Redis, MongoDB, or Memcached.

How do I customize the rate limit response?

Use the handler option to return a custom response body and status code.

Can I skip rate limiting for certain requests?

Yes, use the skip option with a function that evaluates each request.

What headers does express-rate-limit add?

RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset (standard) or X-RateLimit-* (legacy).

Mini Project

Create an Express.js API with: a global rate limiter (50 req/min per IP), auth endpoint limiter (3 req/min per username), data endpoint limiter (200 req/min per user ID), Redis-backed distributed store, custom error responses with upgrade suggestions, and a Locust load test script to verify all limits.

What's Next

  • Learn about Django ratelimit for Python web applications
  • Explore Spring Boot rate limiting with Bucket4j
  • Continue to FastAPI rate limiting with SlowAPI

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro