Discussions

Ask a Question
Back to all

how to implement API rate limiting in Node.js Express

Implementing API rate limiting in a Node.js application built with Express represents a fundamental security and performance measure. It restricts the number of requests a client can make within a defined time window, thereby mitigating abuse, preventing brute-force attacks, reducing server load, and ensuring equitable resource allocation.

The most widely adopted and reliable approach in 2026 utilizes the express-rate-limit middleware, which offers a flexible, well-maintained solution compatible with current Express versions (including Express 5 compatibility as of mid-2025). This middleware supports in-memory storage for simple deployments and external stores (such as Redis) for distributed or scaled environments.

Step-by-Step Implementation

  1. Project Setup and Installation
    Begin with an Express application. If starting fresh, initialize a project and install the required dependencies:

    npm init -y
    npm install express express-rate-limit
    

    For production-grade distributed rate limiting, also install a Redis-compatible store (e.g., rate-limit-redis):

    npm install rate-limit-redis ioredis  # or use 'redis' package
    
  2. Basic Configuration (In-Memory Store)
    Suitable for single-instance or development environments. Create or modify your main application file (e.g., app.js or server.js):

    const express = require('express');
    const { rateLimit } = require('express-rate-limit');
    
    const app = express();
    const port = process.env.PORT || 3000;
    
    // Define the rate limiter
    const apiLimiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      limit: 100,               // Allow 100 requests per window per IP
      standardHeaders: 'draft-8', // Return modern RateLimit header (recommended)
      legacyHeaders: false,     // Disable older X-RateLimit-* headers
      message: { error: 'Too many requests, please try again later.' },
      statusCode: 429           // HTTP 429 Too Many Requests
    });
    
    // Apply globally to all routes
    app.use('/api/', apiLimiter); // Or app.use(apiLimiter) for entire app
    
    // Example route
    app.get('/api/data', (req, res) => {
      res.json({ message: 'Protected API endpoint' });
    });
    
    app.listen(port, () => {
      console.log(`Server running on port ${port}`);
    });
    

    This configuration limits clients to 100 requests every 15 minutes, based on IP address. When exceeded, responses return 429 status with a clear message.

  3. Advanced Configuration Options
    Customize further for specific needs:

    • Per-route limiting: Apply only to sensitive endpoints (e.g., login or password reset).
      app.post('/api/login', loginLimiter, loginHandler);
      
    • Custom key generation: Base limits on API keys or authenticated users instead of IP.
      keyGenerator: (req) => req.headers['x-api-key'] || req.ip,
      
    • Skip certain requests: Ignore successful health checks or OPTIONS requests.
      skipSuccessfulRequests: true,
      skip: (req) => req.path === '/health',
      
  4. Distributed Rate Limiting with Redis
    In clustered, load-balanced, or multi-container setups (common in production), in-memory storage fails to synchronize counts across instances. Use an external Redis store:

    const { RedisStore } = require('rate-limit-redis');
    const Redis = require('ioredis'); // or 'redis' package
    
    const redisClient = new Redis({
      host: 'localhost', // or your Redis host
      port: 6379,
      // password: 'your-password', // if required
    });
    
    const apiLimiter = rateLimit({
      windowMs: 15 * 60 * 1000,
      limit: 100,
      standardHeaders: 'draft-8',
      legacyHeaders: false,
      store: new RedisStore({
        sendCommand: (...args) => redisClient.call(...args),
      }),
    });
    

    This ensures atomic, consistent counting across nodes. Redis provides high performance for increment operations and supports TTL for automatic cleanup.

  5. Best Practices

    • Return informative headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset) to guide clients on retry timing.
    • Combine with exponential backoff in client libraries to handle 429 responses gracefully.
    • Monitor metrics (e.g., via Prometheus or logging) and adjust limits based on observed traffic.
    • Use tiered limits for authenticated vs. unauthenticated users or free vs. paid tiers.
    • Pair with complementary protections, such as slow-down middleware (express-slow-down) for gradual throttling before hard blocks.
    • Validate trusted proxies (app.set('trust proxy', 1)) if behind a load balancer or CDN to ensure accurate IP detection.

This implementation balances simplicity with robustness for most API scenarios. For broader website protection beyond APIs—including static content and forms—consider additional strategies outlined in this guide: how to enable rate limiting to protect your website.

Should you require examples with TypeScript, integration with authentication middleware, or handling of specific algorithms (e.g., sliding window), provide further details for tailored guidance.