Skip to content

Rate Limiting Middleware — Complete Implementation Guide

DodaTech Updated 2026-06-28 4 min read

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

Rate limiting middleware protects your API by restricting how many requests a client can make within a time window, preventing abuse and ensuring fair resource allocation across all users.

What You'll Learn

By the end of this tutorial, you will implement rate limiting middleware using the Express-rate-limit package, configure per-route limits, and handle rate limit exceeded responses.

Why It Matters

Without rate limiting, a single client can overwhelm your server with requests, causing downtime for all users. DodaTech's APIs use rate limiting to prevent brute force attacks and ensure service availability.

Real-World Use

Doda Browser's API allows 100 requests per minute for authenticated users and 10 requests per minute for unauthenticated users, enforced through rate limiting middleware.

Rate Limiting Middleware Learning Path

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

Basic Rate Limiting

The express-rate-limit package provides configurable rate limiting middleware that tracks requests by IP address.

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

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

app.use(limiter);

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

app.listen(3000);

Expected behavior: After 100 requests within 15 minutes, the client receives a 429 status with the error message.

Per-Route Rate Limits

Different endpoints need different limits. Login endpoints need stricter limits to prevent brute force attacks, while read-only endpoints can be more permissive.

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

const globalLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 60,
  message: { error: "Too many requests" }
});

const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: { error: "Too many login attempts. Try again in 15 minutes." }
});

const apiLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 30,
  message: { error: "API rate limit exceeded" }
});

app.use(globalLimiter);

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

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

app.listen(3000);

Custom Key Generator

By default, rate limiting uses the client's IP address. For authenticated APIs, you may want to rate limit by user ID instead.

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

const userLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 100,
  keyGenerator: (req) => {
    if (req.user && req.user.id) {
      return `user:${req.user.id}`;
    }
    return `ip:${req.ip}`;
  },
  handler: (req, res) => {
    res.status(429).json({
      error: "Rate limit exceeded",
      userId: req.user?.id || "anonymous"
    });
  }
});

app.use(userLimiter);

Expected behavior: Each authenticated user gets 100 requests per minute, regardless of which IP they connect from. Unauthenticated users are limited by IP.

Common Mistakes

  1. Rate limiting by IP behind a proxy -- If your app is behind a reverse proxy, req.ip may be the proxy's IP. Set app.set("trust proxy", true).

  2. Not distinguishing authenticated and anonymous users -- Authenticated users should get higher limits. Use the key generator to check for user context.

  3. Using memory store in distributed deployments -- In-memory rate limiting does not sync across servers. Use Redis for multi-instance deployments.

  4. Not returning Retry-After headers -- Clients need to know when they can retry. Include the Retry-After header in 429 responses.

  5. Rate limiting all endpoints equally -- Login attempts need strict limits. Read-only GET requests can be more permissive.

Practice Questions

  1. What HTTP status code does rate limiting middleware return? 429 Too Many Requests.

  2. How do you rate limit by API key instead of IP? Use the keyGenerator option to extract the API key from headers or query parameters.

  3. Why use Redis for rate limiting in production? Redis provides a shared, atomic counter across all application instances, ensuring consistent rate limiting.

  4. Challenge: Implement rate limiting that increases limits for verified users.

const userLimiter = rateLimit({
  windowMs: 60000,
  max: (req) => req.user?.verified ? 200 : 20,
  keyGenerator: (req) => req.user?.id || req.ip
});

FAQ

What is the difference between rate limiting and throttling?

Rate limiting blocks requests exceeding a threshold. Throttling slows down requests but does not block them.

Can rate limiting be bypassed?

Using distributed attacks from many IPs can bypass IP-based limiting. Combine with user-based limiting and CAPTCHA for stronger protection.

Should I rate limit POST and GET differently?

Yes. Write operations typically need stricter limits than read operations to prevent data abuse.

How do I test rate limiting?

Write integration tests that send many requests in sequence and verify the 429 response after the limit is exceeded.

What headers does rate limiting middleware add?

RateLimit-Limit (window limit), RateLimit-Remaining (remaining requests), and Retry-After (seconds until reset).

Mini Project

Build a complete rate limiting system with global limits, per-route limits for auth endpoints, Redis persistence, and custom error responses.

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

const client = redis.createClient({ url: process.env.REDIS_URL });

const globalLimiter = rateLimit({
  store: new RedisStore({
    sendCommand: (...args) => client.sendCommand(args)
  }),
  windowMs: 60 * 1000,
  max: 60,
  message: { error: "Rate limit exceeded" }
});

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

app.use(globalLimiter);
app.post("/login", authLimiter, (req, res) => {
  res.json({ success: true });
});

app.listen(3000);

What's Next

Now that you understand rate limiting middleware, explore composing multiple middleware functions together. Then learn about handling asynchronous operations in middleware.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro