Rate Limiting & Throttling · learncode.live

Why Rate Limiting?

Rate limiting protects your API from abuse - brute-force attacks, DDoS, scraping, and runaway clients.

Without rate limiting:                With rate limiting:
                                        │
  Attacker ─── 10,000 req/s ──────→     │ Attacker ─── 10,000 req/s ──────→
  Server: CPU 100%, DB overwhelmed      │ Rate limiter blocks 9,900
  All users: 503 Service Unavailable    │ Legitimate users: served normally

Basic Rate Limiting with express-rate-limit

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

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

app.use(limiter);

Configuration Options

const limiter = rateLimit({
  // Time window in milliseconds
  windowMs: 15 * 60 * 1000,

  // Max requests per window
  max: 100,

  // Response when limit is reached
  message: { error: 'Rate limit exceeded' },

  // Status code when limit is reached
  statusCode: 429,

  // Response headers (standard: RateLimit-*, legacy: X-RateLimit-*)
  standardHeaders: true,
  legacyHeaders: false,

  // Key generator (default: req.ip)
  keyGenerator: (req) => {
    return req.user?.id || req.ip;
  },

  // Skip rate limiting for certain requests
  skip: (req) => {
    return req.path === '/health' || req.ip === '127.0.0.1';
  },

  // Skip failed requests (don't count 4xx/5xx responses)
  skipSuccessfulRequests: false,

  // Handler when limit is reached
  handler: (req, res) => {
    res.status(429).json({
      error: 'Too many requests',
      retryAfter: Math.ceil(req.rateLimit.resetTime / 1000 - Date.now() / 1000)
    });
  }
});

Tiered Rate Limiting

Apply different limits to different endpoints:

// Global: 100 req/15min
app.use(rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100
}));

// Strict: Auth endpoints - 5 attempts/15min
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: { error: 'Too many login attempts. Account locked temporarily.' },
  skipSuccessfulRequests: true // Only count failures
});
app.use('/api/login', authLimiter);
app.use('/api/register', authLimiter);

// Moderate: API endpoints - 60 req/min
const apiLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 60
});
app.use('/api/', apiLimiter);

// Generous: Public endpoints - 1000 req/min
const publicLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 1000
});
app.use('/api/public', publicLimiter);

Rate Limiting by User Role

function roleBasedLimiter() {
  return (req, res, next) => {
    const limits = {
      admin: { windowMs: 60 * 1000, max: 1000 },
      user: { windowMs: 60 * 1000, max: 100 },
      anonymous: { windowMs: 60 * 1000, max: 10 }
    };

    const role = req.user?.role || 'anonymous';
    const config = limits[role];

    const limiter = rateLimit({
      windowMs: config.windowMs,
      max: config.max,
      keyGenerator: () => req.user?.id || req.ip,
      message: { error: `Rate limit exceeded for ${role} role` }
    });

    return limiter(req, res, next);
  };
}

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

Rate Limiting with Redis (Production)

For multi-server deployments, store rate limit data in Redis:

npm install rate-limit-redis ioredis
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis').default;
const { createClient } = require('ioredis');

const redisClient = createClient({
  url: process.env.REDIS_URL || 'redis://localhost:6379'
});

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

app.use(limiter);

Rate Limit Headers

When standardHeaders: true, clients receive:

RateLimit-Limit: 100
RateLimit-Remaining: 87
RateLimit-Reset: 1689000000
Retry-After: 360
// Client-side: respect rate limits
async function apiCall(url) {
  const res = await fetch(url);

  if (res.status === 429) {
    const retryAfter = res.headers.get('Retry-After');
    console.log(`Rate limited. Retry after ${retryAfter} seconds`);
    await new Promise(r => setTimeout(r, retryAfter * 1000));
    return apiCall(url); // Retry after waiting
  }

  return res.json();
}

IP-Based Blocking

const blockedIPs = new Set();

const limiter = rateLimit({
  windowMs: 60 * 1000,
  max: 100,
  keyGenerator: (req) => req.ip,
  handler: (req, res) => {
    // After 3 strikes within the window, block the IP for 1 hour
    const key = req.ip;
    const strikeCount = req.rateLimit.current - req.rateLimit.max;

    if (strikeCount >= 3) {
      blockedIPs.add(key);
      setTimeout(() => blockedIPs.delete(key), 60 * 60 * 1000);
    }

    res.status(429).json({
      error: 'Too many requests',
      blocked: blockedIPs.has(key)
    });
  }
});

// Middleware to check blocked IPs
app.use((req, res, next) => {
  if (blockedIPs.has(req.ip)) {
    return res.status(403).json({ error: 'Your IP has been blocked' });
  }
  next();
});

Speed Limiter (Slow Down Instead of Block)

Use express-slow-down to slow down excessive clients instead of blocking them:

npm install express-slow-down
const slowDown = require('express-slow-down');

const speedLimiter = slowDown({
  windowMs: 15 * 60 * 1000,
  delayAfter: 50,       // Allow 50 requests without delay
  delayMs: (hits) => hits * 100, // Add 100ms delay per hit after threshold
  maxDelayMs: 5000       // Max 5 second delay
});

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

Burst Protection

Allow short bursts but limit sustained traffic:

const burstProtection = rateLimit({
  windowMs: 1 * 1000,   // 1 second window
  max: 10,               // 10 requests per second
  message: { error: 'Too fast. Slow down.' }
});

const sustainedLimiter = rateLimit({
  windowMs: 60 * 1000,  // 1 minute window
  max: 100,              // 100 requests per minute
  message: { error: 'Sustained rate limit exceeded' }
});

app.use('/api', burstProtection, sustainedLimiter);

DDoS Mitigation Strategy

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

const app = express();

// Layer 1: Connection limiting (via reverse proxy usually)

// Layer 2: Global rate limit
app.use(rateLimit({
  windowMs: 60 * 1000,
  max: 200,
  message: { error: 'Global rate limit exceeded' }
}));

// Layer 3: Endpoint-specific limits
app.use('/api/login', rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: { error: 'Too many login attempts' },
  skipSuccessfulRequests: true
}));

app.use('/api/search', rateLimit({
  windowMs: 60 * 1000,
  max: 30,
  message: { error: 'Search rate limit exceeded' }
}));

// Layer 4: Slow down excessive clients
app.use('/api', slowDown({
  windowMs: 60 * 1000,
  delayAfter: 100,
  delayMs: 500
}));

// Layer 5: Body size limits
app.use(express.json({ limit: '100kb' }));

// Layer 6: Payload validation
app.use((req, res, next) => {
  if (req.is('application/json') && typeof req.body !== 'object') {
    return res.status(400).json({ error: 'Invalid payload' });
  }
  next();
});

Rate Limit by IP or User

// Anonymous users: limit by IP
// Authenticated users: limit by userId
function smartKeyGenerator(req) {
  if (req.user?.id) {
    return `user:${req.user.id}`;
  }
  return `ip:${req.ip}`;
}

const limiter = rateLimit({
  windowMs: 60 * 1000,
  max: 60,
  keyGenerator: smartKeyGenerator
});

Testing Rate Limiting

const request = require('supertest');

describe('Rate Limiting', () => {
  it('should allow requests under the limit', async () => {
    const res = await request(app).get('/api/test');
    expect(res.status).toBe(200);
    expect(res.headers['ratelimit-remaining']).toBeDefined();
  });

  it('should block requests over the limit', async () => {
    const requests = Array(105).fill().map(() =>
      request(app).get('/api/test')
    );

    const responses = await Promise.all(requests);
    const blocked = responses.filter(r => r.status === 429);

    expect(blocked.length).toBeGreaterThan(0);
  });
});

Key Takeaways

  • Use express-rate-limit with windowMs and max for basic rate limiting
  • Apply tiered limits: strict for auth, moderate for API, generous for public
  • Use RedisStore for rate limiting across multiple server instances
  • Return rate limit headers (RateLimit-*) so clients can self-regulate
  • Use express-slow-down to slow excessive clients instead of blocking
  • Combine rate limiting with body size limits, validation, and DDoS protection
  • Use smart key generators - by userId for authenticated users, by IP for anonymous
  • Always test rate limiting with integration tests
Courses