Skip to content

Rate Limiting in Express — Complete Implementation Guide

DodaTech Updated 2026-06-28 5 min read

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

Rate limiting in Express is implemented using the express-rate-limit middleware, which provides configurable window-based rate limiting with support for custom stores and per-route configuration.

What You'll Learn

By the end of this tutorial, you will configure express-rate-limit for different use cases, use custom stores for distributed deployments, and handle rate limit violations gracefully.

Why It Matters

Express is the most popular Node.js framework, and express-rate-limit is its standard rate limiting middleware. DodaTech uses it in every Express-based API.

Real-World Use

DodaZIP's Express API uses express-rate-limit with a Redis store to enforce per-user rate limits across multiple server instances in a Kubernetes cluster.

Express Rate Limiting Learning Path

flowchart LR
  A[Sliding Window] --> B[Express Rate Limiting]
  B --> C[express-rate-limit]
  C --> D[Redis Store]
  B --> E{You Are Here}
  style E fill:#f90,color:#fff

Basic Configuration

Install and configure express-rate-limit with default options. This creates a fixed window rate limiter using in-memory storage.

npm install express-rate-limit
const rateLimit = require("express-rate-limit");
const express = require("express");
const app = express();

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  message: {
    error: "Too many requests, please try again later."
  },
  standardHeaders: true,
  legacyHeaders: false
});

app.use(limiter);

app.get("/", (req, res) => {
  res.json({ message: "Welcome" });
});

app.listen(3000);

Per-Route Configuration

Different routes need different rate limits. Use route-specific limiter instances for fine-grained control.

const express = require("express");
const rateLimit = require("express-rate-limit");
const app = express();

const globalLimiter = rateLimit({
  windowMs: 60000,
  max: 60,
  message: { error: "Global limit exceeded" }
});

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  skipSuccessfulRequests: true,
  message: { error: "Too many login attempts" }
});

const apiLimiter = rateLimit({
  windowMs: 60000,
  max: 30,
  keyGenerator: (req) => req.user?.id || req.ip
});

app.use(globalLimiter);

app.post("/login", authLimiter, (req, res) => {
  res.json({ token: "auth-token" });
});

app.get("/api/data", apiLimiter, (req, res) => {
  res.json({ data: "api response" });
});

app.listen(3000);

Custom Store with Redis

In-memory storage does not work across multiple server instances. Use the rate-limit-redis package for distributed rate limiting.

npm install rate-limit-redis ioredis
const rateLimit = require("express-rate-limit");
const RedisStore = require("rate-limit-redis");
const Redis = require("ioredis");
const express = require("express");
const app = express();

const redis = new Redis({
  host: process.env.REDIS_HOST || "localhost",
  port: process.env.REDIS_PORT || 6379
});

const limiter = rateLimit({
  store: new RedisStore({
    sendCommand: (...args) => redis.call(...args)
  }),
  windowMs: 60000,
  max: 100,
  message: { error: "Rate limit exceeded" }
});

app.use(limiter);
app.get("/", (req, res) => res.json({ ok: true }));
app.listen(3000);

Skipping Rate Limiting

Some requests should not count toward rate limits, such as health checks or internal service calls.

const limiter = rateLimit({
  windowMs: 60000,
  max: 100,
  skip: (req) => {
    if (req.path === "/health") return true;
    if (req.headers["x-internal-request"]) return true;
    return false;
  },
  keyGenerator: (req) => {
    if (req.user?.id) return `user:${req.user.id}`;
    if (req.headers["x-api-key"]) return `apikey:${req.headers["x-api-key"]}`;
    return `ip:${req.ip}`;
  }
});

Common Mistakes

  1. Not setting trust proxy -- Behind a reverse proxy, req.ip is the proxy's IP. Set app.set("trust proxy", true).

  2. Using default memory store in production -- Memory does not scale across instances. Use Redis or another shared store.

  3. Not configuring skipSuccessfulRequests -- Failed login attempts should count. Successful logins should not count to prevent lockout.

  4. Setting max too low or too high -- Too low blocks legitimate users. Too high does not prevent abuse. Base limits on traffic analysis.

  5. Not returning rate limit headers -- Standard headers help clients self-regulate. Enable them with standardHeaders: true.

Practice Questions

  1. How do you configure different rate limits for authenticated vs anonymous users? Use a custom keyGenerator that distinguishes between user IDs and IP addresses, with different limiter instances.

  2. What happens when the Redis store is unavailable? express-rate-limit throws an error. Implement a fallback memory store or use a circuit breaker.

  3. How do you whitelist specific IPs from rate limiting? Use the skip option to bypass rate limiting for whitelisted IPs or internal network ranges.

  4. Challenge: Configure rate limiting that increases limits during off-peak hours.

const limiter = rateLimit({
  windowMs: 60000,
  max: (req) => {
    const hour = new Date().getHours();
    return hour >= 2 && hour <= 6 ? 200 : 100;
  }
});

FAQ

Does express-rate-limit work with Express 5?

Yes. express-rate-limit is compatible with both Express 4 and Express 5.

Can I use express-rate-limit with WebSockets?

express-rate-limit works with HTTP requests only. WebSocket rate limiting requires custom implementation.

How do I reset rate limits for a specific user?

Delete the user's key from the store. For memory store, delete the key from the internal map. For Redis, DEL the key.

What headers does express-rate-limit set?

With standardHeaders: true, it sets RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset headers.

How do I test rate limiting locally?

Set low limits (max: 3, windowMs: 10000) and send rapid requests using curl to verify 429 responses.

Mini Project

Build a complete rate limiting setup with global limits, per-route limits, Redis store, skip conditions for health checks, and rate limit headers.

const express = require("express");
const rateLimit = require("express-rate-limit");
const RedisStore = require("rate-limit-redis");
const Redis = require("ioredis");
const app = express();

app.set("trust proxy", 1);

const redis = new Redis(process.env.REDIS_URL);

const createLimiter = (opts) => rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
  standardHeaders: true,
  legacyHeaders: false,
  ...opts
});

const global = createLimiter({ windowMs: 60000, max: 60 });
const auth = createLimiter({ windowMs: 900000, max: 5, skipSuccessfulRequests: true });
const api = createLimiter({ windowMs: 60000, max: 30, keyGenerator: (req) => req.user?.id || req.ip });

app.use(global);
app.post("/auth/login", auth);
app.use("/api", api);
app.get("/health", (req, res) => res.json({ status: "ok" }));

app.use((err, req, res, next) => {
  if (err.code === "LIMIT_UNEXPECTED_EOF") {
    return res.status(429).json({ error: "Rate limit store error" });
  }
  next(err);
});

app.listen(3000);

What's Next

Now that you understand rate limiting in Express, explore using Redis for production rate limiting. Then learn about rate limiting across multiple servers.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro