Navigation

Node.js

How to Implement Rate Limiting with express-rate-limit

Protect your Express.js API from abuse with rate limiting middleware. Prevent spam, DDoS attacks, and resource exhaustion using express-rate-limit package.

Table Of Contents

Problem

You need to protect your Express.js application from abuse by limiting the number of requests users can make within a specific time window to prevent spam, brute force attacks, and server overload.

Solution

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

// 1. Basic Rate Limiting
const basicLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Limit each IP to 100 requests per windowMs
  message: {
    error: 'Too many requests',
    message: 'You have exceeded the rate limit. Try again later.',
    retryAfter: '15 minutes'
  },
  standardHeaders: true, // Return rate limit info in headers
  legacyHeaders: false, // Disable X-RateLimit-* headers
});

// Apply to all routes
app.use(basicLimiter);

// 2. Strict Rate Limiting for Authentication
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // Only 5 login attempts per IP
  message: {
    error: 'Too many login attempts',
    message: 'Account temporarily locked. Try again in 15 minutes.'
  },
  skipSuccessfulRequests: true, // Don't count successful logins
});

app.post('/auth/login', authLimiter, (req, res) => {
  // Simulate login logic
  const { email, password } = req.body;
  
  if (email === 'user@example.com' && password === 'password') {
    res.json({ success: true, token: 'jwt-token-here' });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

// 3. API Key Based Rate Limiting
const createApiLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: (req) => {
    // Different limits based on API key tier
    const apiKey = req.headers['x-api-key'];
    if (apiKey === 'premium-key') return 10000;
    if (apiKey === 'standard-key') return 1000;
    return 100; // Free tier
  },
  keyGenerator: (req) => {
    // Use API key instead of IP for rate limiting
    return req.headers['x-api-key'] || req.ip;
  },
  message: (req) => ({
    error: 'API rate limit exceeded',
    limit: typeof req.rateLimit?.max === 'function' 
      ? req.rateLimit.max(req) 
      : req.rateLimit?.max,
    remaining: req.rateLimit?.remaining,
    resetTime: req.rateLimit?.resetTime
  })
});

app.get('/api/data', createApiLimiter, (req, res) => {
  res.json({
    data: 'Your API data here',
    rateLimit: {
      limit: req.rateLimit.limit,
      remaining: req.rateLimit.remaining,
      reset: new Date(req.rateLimit.resetTime)
    }
  });
});

// 4. Different Limits for Different Routes
const uploadLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 3, // Only 3 uploads per minute
  message: 'Upload limit exceeded. Please wait before uploading again.'
});

const searchLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 30, // 30 searches per minute
  message: 'Search limit exceeded. Please slow down.'
});

app.post('/upload', uploadLimiter, (req, res) => {
  res.json({ message: 'File uploaded successfully' });
});

app.get('/search', searchLimiter, (req, res) => {
  const query = req.query.q;
  res.json({ 
    query,
    results: [`Result 1 for ${query}`, `Result 2 for ${query}`]
  });
});

// 5. Custom Store (using Redis for distributed systems)
// Uncomment if using Redis
/*
const RedisStore = require('rate-limit-redis');
const redis = require('redis');
const redisClient = redis.createClient();

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

// 6. Skip Certain Requests
const smartLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  skip: (req) => {
    // Skip rate limiting for trusted IPs
    const trustedIPs = ['127.0.0.1', '::1'];
    return trustedIPs.includes(req.ip);
  },
  skipFailedRequests: true, // Don't count failed requests
});

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

// 7. Dynamic Rate Limiting
const dynamicLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: (req) => {
    // Higher limits during off-peak hours
    const hour = new Date().getHours();
    if (hour >= 2 && hour <= 6) {
      return 200; // Off-peak hours
    }
    return 50; // Peak hours
  },
  message: (req) => {
    const hour = new Date().getHours();
    const isPeakHour = !(hour >= 2 && hour <= 6);
    return {
      error: 'Rate limit exceeded',
      message: isPeakHour 
        ? 'Peak hour limits apply (50 req/min)'
        : 'Off-peak limits apply (200 req/min)'
    };
  }
});

// 8. Rate Limit Headers and Monitoring
app.use((req, res, next) => {
  // Add custom rate limit headers
  if (req.rateLimit) {
    res.set({
      'X-RateLimit-Limit': req.rateLimit.limit,
      'X-RateLimit-Remaining': req.rateLimit.remaining,
      'X-RateLimit-Reset': new Date(req.rateLimit.resetTime).toISOString()
    });
  }
  next();
});

app.get('/status', (req, res) => {
  res.json({
    message: 'Server is running',
    timestamp: new Date().toISOString(),
    rateLimit: req.rateLimit ? {
      limit: req.rateLimit.limit,
      remaining: req.rateLimit.remaining,
      resetTime: new Date(req.rateLimit.resetTime).toISOString()
    } : 'No rate limit applied'
  });
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Install express-rate-limit:

npm install express-rate-limit
# For Redis store (optional)
npm install rate-limit-redis redis

Test rate limiting:

# Test basic rate limiting
for i in {1..10}; do curl http://localhost:3000/status; echo; done

# Test auth rate limiting
for i in {1..6}; do 
  curl -X POST http://localhost:3000/auth/login \
    -H "Content-Type: application/json" \
    -d '{"email":"wrong","password":"wrong"}'
  echo
done

Explanation

express-rate-limit tracks requests per IP address within a time window. Configure windowMs for the time period and max for request limits. The middleware automatically returns 429 (Too Many Requests) when limits are exceeded.

Use different limiters for different routes based on sensitivity - stricter limits for authentication and looser limits for public APIs. For distributed systems, use Redis store to share rate limit data across multiple server instances.

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Node.js