Rate Limiting & Abuse Control

Tiered limits, a shared store for multi-instance deployments, and trust proxy so you throttle the real client, not your load balancer.

A single global limiter is a blunt instrument. Real protection is tiered: a generous ceiling on the whole API, a strict one on /auth/login and /auth/forgot-password, and perhaps a per-user quota on expensive endpoints.

The multi-instance trap

express-rate-limit's default store is in-memory. Run three instances behind a load balancer and you have three independent counters — effectively triple your intended limit, and the count resets on every deploy. In production, back it with a shared store like Redis.

Get the client IP right

Behind a proxy, every request appears to come from the proxy's IP, so one bad actor would rate-limit everyone. You must set app.set('trust proxy', 1) so Express reads the real IP from X-Forwarded-For.

app.set('trust proxy', 1); // trust exactly one hop (your LB)

Example

Example · javascript
import express from 'express';
import { rateLimit } from 'express-rate-limit';
// import { RedisStore } from 'rate-limit-redis'; // for multi-instance

const app = express();

// One hop: our load balancer. Never use `true` in production.
app.set('trust proxy', 1);

// Generous ceiling for the whole API.
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  limit: 1000,
  standardHeaders: 'draft-7',   // RateLimit-* headers
  legacyHeaders: false,
  // store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
});

// Strict limiter for credential endpoints — slows brute force to a crawl.
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  limit: 5,
  skipSuccessfulRequests: true, // only failed attempts count toward the limit
  message: { error: 'Too many attempts, try again later.' },
});

app.use('/api', apiLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/forgot-password', authLimiter);

When to use it

  • A multi-tier API applies a global 1000 req/hr rate limit, a stricter 5 req/15min limit on the login endpoint, and a 10 req/min limit on the password-reset endpoint.
  • A team stores rate limit counters in Redis so the limit is enforced consistently when the Express app runs in a multi-process cluster.
  • A developer implements a sliding window rate limiter to prevent clients from bursting 100 requests in the last second of each window.

More examples

Layered rate limits

Applies a lenient global limit to all API routes and a strict limit on the login endpoint to defend against brute-force.

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

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

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

Redis-backed rate limiter

Uses rate-limit-redis to store counters in Redis so multiple Express processes share the same rate limit state.

Example · js
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const { createClient } = require('redis');

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

const limiter = rateLimit({
  windowMs: 60 * 1000,
  max: 100,
  store: new RedisStore({ sendCommand: (...args) => client.sendCommand(args) }),
});

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

IP + user key rate limiting

Uses a custom keyGenerator to rate-limit authenticated users by their ID and unauthenticated requests by IP.

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

const perUserLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 60,
  keyGenerator: (req) => req.user?.id || req.ip, // authenticated users by ID
});

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

Discussion

  • Be the first to comment on this lesson.