Rate Limiting & Throttling

Difficulty: Intermediate

Question

How do you implement rate limiting in a Node.js API? Explain different algorithms and production considerations.

Answer

Rate limiting restricts the number of requests a client can make within a time window. It protects APIs from abuse, brute force attacks, and accidental overload. Without it, a single client can monopolize server resources.

Common algorithms include: fixed window (simple counter per time window), sliding window (more accurate, counts requests in a rolling window), token bucket (allows bursts), and leaky bucket (smooths traffic). Each has tradeoffs between simplicity, accuracy, and memory usage.

The express-rate-limit package provides easy in-memory rate limiting for single-server setups. For multi-server deployments, use a Redis-backed store so all instances share the same counters.

Different endpoints need different limits: authentication endpoints (strict, e.g., 5 attempts/15 min), API endpoints (moderate, e.g., 100 requests/min), and public pages (lenient, e.g., 1000 requests/min). Always return rate limit headers so clients can adapt.

Code examples

express-rate-limit Setup

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

// General API limiter
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,                  // 100 requests per window
  standardHeaders: true,     // Return rate limit info in headers
  legacyHeaders: false,
  message: {
    error: 'Too many requests, please try again later',
    retryAfter: '15 minutes',
  },
});

// Strict limiter for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: { error: 'Too many login attempts. Try again in 15 minutes.' },
  skipSuccessfulRequests: true, // Only count failed attempts
});

// Apply limiters
app.use('/api', apiLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);

// Response headers sent:
// RateLimit-Limit: 100
// RateLimit-Remaining: 95
// RateLimit-Reset: 1705313100

Different limiters for different routes. skipSuccessfulRequests only counts failures (good for login). Headers tell clients their quota.

Redis-Backed Rate Limiting (Multi-Server)

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

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

const limiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 60,
  standardHeaders: true,
  store: new RedisStore({
    sendCommand: (...args) => redis.call(...args),
    prefix: 'rl:', // Redis key prefix
  }),
  keyGenerator: (req) => {
    // Rate limit by user ID (authenticated) or IP (anonymous)
    return req.user?.id?.toString() || req.ip;
  },
  skip: (req) => {
    // Skip rate limiting for admin users
    return req.user?.role === 'admin';
  },
});

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

Redis store shares rate limit state across all server instances. keyGenerator customizes how clients are identified. skip exempts certain users.

Custom Sliding Window Implementation

const Redis = require('ioredis');
const redis = new Redis();

// Sliding window rate limiter using Redis sorted sets
async function slidingWindowRateLimit(key, limit, windowSec) {
  const now = Date.now();
  const windowStart = now - windowSec * 1000;

  const multi = redis.multi();
  // Remove old entries outside the window
  multi.zremrangebyscore(key, 0, windowStart);
  // Add current request
  multi.zadd(key, now, `${now}-${Math.random()}`);
  // Count requests in window
  multi.zcard(key);
  // Set expiry on the key
  multi.expire(key, windowSec);

  const results = await multi.exec();
  const count = results[2][1];

  return {
    allowed: count <= limit,
    remaining: Math.max(0, limit - count),
    resetAt: new Date(now + windowSec * 1000),
  };
}

// Middleware
function rateLimitMiddleware(limit, windowSec) {
  return async (req, res, next) => {
    const key = `ratelimit:${req.ip}`;
    const result = await slidingWindowRateLimit(key, limit, windowSec);

    res.set('X-RateLimit-Limit', limit);
    res.set('X-RateLimit-Remaining', result.remaining);

    if (!result.allowed) {
      return res.status(429).json({ error: 'Rate limit exceeded' });
    }
    next();
  };
}

Sliding window uses Redis sorted sets with timestamps as scores. This is more accurate than fixed windows and prevents burst-at-boundary attacks.

Key points

Concepts covered

Rate Limiting, express-rate-limit, Sliding Window, Token Bucket, Redis Store