npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

express-multi-limiter

v2.0.0

Published

A high-performance, multi-algorithm rate limiter and WAF for Express

Readme

express-multi-limiter 🚀🛡️

npm version License: MIT Node.js CI Coverage

A high-performance, enterprise-grade, multi-algorithm rate limiter and lightweight Web Application Firewall (WAF) for Express.js.

While most rate limiters offer a single basic counter, express-multi-limiter gives you the choice of five different mathematical rate-limiting algorithms, supports distributed databases (like Redis), features a unique Penalty Box system to automatically ban abusive users, and implements Strict Mutex Locking to prevent concurrency race conditions.


📑 Table of Contents

  1. Why express-multi-limiter?
  2. Core Features
  3. Installation
  4. Quick Start (In-Memory)
  5. Advanced: Redis & Distributed Clusters
  6. Configuration API Reference
  7. The 5 Algorithms (Deep Dive)
  8. The Penalty Box (WAF Abuse Prevention)
  9. Dynamic Tiering & Cost-Based Limiting
  10. Event Telemetry & Monitoring
  11. RFC 9333 Standard Headers
  12. Architecture: Solving Race Conditions (Mutex)
  13. Comparison with Competitors
  14. Contributing
  15. License

🤔 Why express-multi-limiter?

Modern APIs face diverse traffic patterns. A /login route needs strict burst protection, while an /export-data route needs steady, smooth rate limiting. Basic counters fall short.

Furthermore, under high load, standard Node.js rate limiters suffer from Race Conditions. If two requests from the same IP hit the server at the exact same millisecond, they both read a count of 5, both increment to 6, and write 6. One request bypasses the limit entirely.

express-multi-limiter solves this by introducing Atomic Promise-based Mutex Queues in memory, and native support for Lua-scripted atomic execution in distributed stores like Redis.


✨ Core Features

  • 5 Rate Limiting Algorithms: Fixed Window, Sliding Window Counter, Sliding Window Log, Token Bucket, and Leaky Bucket.
  • The Penalty Box (WAF): Automatically send repeat offenders to "jail," immediately rejecting their requests with a 403 Forbidden status for a set duration.
  • True Atomic Execution (No Race Conditions): Uses Promise Mutex chaining to guarantee chronological state updates under high concurrency.
  • Distributed Store Ready: Plugs seamlessly into Redis or Memcached for Kubernetes/PM2 multi-server clusters.
  • Dynamic / Tiered Limits: Apply different limits to different users (e.g., Free vs. Premium tiers) dynamically per request.
  • Cost-Based Limiting: Assign higher token "costs" to resource-intensive routes.
  • IP Allowlist & Blocklist: Immediately block malicious IPs or bypass limits for internal microservices using O(1) Set lookups.
  • Graceful Degradation (Fail-Open): If your Redis server crashes, the limiter can temporarily allow traffic so your app doesn't go offline.
  • RFC 9333 Compliance: Injects modern, standardized rate-limit HTTP headers.

📦 Installation

Install via your preferred package manager:

# npm
npm install express-multi-limiter

🚀 Quick Start (In-Memory)

By default, the limiter runs in memory. This is perfect for single-server setups or local development.

const express = require('express');
const { createLimiter } = require('express-multi-limiter');

const app = express();

// Create the limiter middleware
const apiLimiter = createLimiter({
    algorithm: 'sliding-counter', // Smoothly handles traffic spikes
    windowMs: 60 * 1000,          // 1 minute time window
    limit: 100,                   // 100 requests per minute

    // Enable the Penalty Box to punish abusers
    penaltyBox: {
        enabled: true,
        maxViolations: 3,           // If they hit the limit 3 times...
        jailTimeMs: 15 * 60 * 1000  // ...ban them entirely for 15 minutes
    }
});

// Apply to all /api routes
app.use('/api/', apiLimiter);

app.get('/api/data', (req, res) => {
    res.json({ message: "Success!" });
});

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

🌍 Advanced: Redis & Distributed Clusters

If you are running multiple instances of your Node.js app (e.g., Kubernetes, AWS ECS, PM2), you must share the rate-limit state across all servers.

Here is how you connect express-multi-limiter to Redis using the official redis npm package:

const express = require('express');
const { createClient } = require('redis');
const { createLimiter } = require('express-multi-limiter');

const app = express();

// 1. Initialize Redis Client
const redisClient = createClient({ url: 'redis://localhost:6379' });
redisClient.connect().catch(console.error);

// 2. Create an Atomic Store Adapter
const redisStore = {
    // Standard getters and setters
    get: async (key) => {
        const data = await redisClient.get(key);
        return data ? JSON.parse(data) : undefined;
    },
    set: async (key, value) => {
        const ttlSeconds = value.expiresAt 
            ? Math.max(1, Math.ceil((value.expiresAt - Date.now()) / 1000)) 
            : 60;
        await redisClient.setEx(key, ttlSeconds, JSON.stringify(value));
    },
    delete: async (key) => await redisClient.del(key),

    // Advanced: Implement atomic execution using Redis MULTI/EXEC or Lua
    // This prevents race conditions across different physical servers
    executeAtomic: async (key, logicFn) => {
        // Simple example locking mechanism (Use robust packages like Redlock in production)
        const lockKey = `lock:${key}`;
        const acquired = await redisClient.set(lockKey, 'locked', { NX: true, EX: 2 });
        
        if (!acquired) {
            // Wait and retry logic goes here
            throw new Error("Resource busy"); 
        }

        try {
            const data = await redisClient.get(key);
            const current = data ? JSON.parse(data) : undefined;
            const { state, result } = logicFn(current); // Sync execution
            
            if (state !== undefined) {
                const ttl = state.expiresAt ? Math.ceil((state.expiresAt - Date.now()) / 1000) : 60;
                await redisClient.setEx(key, ttl, JSON.stringify(state));
            }
            return result;
        } finally {
            await redisClient.del(lockKey);
        }
    }
};

// 3. Pass the store to the limiter
const distributedLimiter = createLimiter({
    store: redisStore,
    failOpen: true, // If Redis goes offline, allow traffic to pass (Graceful Degradation)
    algorithm: 'token-bucket',
    limit: 50,
    windowMs: 60000
});

app.use(distributedLimiter);

⚙️ Configuration API Reference

The createLimiter(config) function accepts the following configuration options:

| Property | Type | Default | Description | |----------|------|---------|-------------| | algorithm | string | 'sliding-counter' | The mathematical rate-limiting engine. Supported values are fixed-window, sliding-counter, sliding-log, token-bucket, and leaky-bucket. | | limit | number \| Function | 10 | Maximum requests allowed in the configured time window. Can also be an asynchronous function for dynamic limits. | | windowMs | number \| Function | 60000 | The time window, in milliseconds. Can also be an asynchronous function. | | cost | number \| Function | 1 | Token cost per request. Useful for assigning higher costs to resource-intensive endpoints. | | store | Object | MemoryStore | Database adapter implementing the required storage interface (get, set, delete, executeAtomic). | | penaltyBox | Object | {} | Configuration object for the built-in WAF Penalty Box system. | | allowList | Array<string> | [] | Exact IP addresses that bypass rate limiting entirely. Lookups are performed in O(1) using a Set. | | blockList | Array<string> | [] | Exact IP addresses that are immediately rejected with 403 Forbidden before any rate-limiting logic executes. | | failOpen | boolean | true | If the backing store throws an error (e.g., Redis outage), requests are allowed instead of failing the API. | | keyGenerator | Function | (req) => req.ip | Generates the unique identifier used for rate limiting (e.g., req.user.id, API key, or IP address). | | skip | Function | () => false | Return true to bypass rate limiting for a request (e.g., health checks or internal routes). | | handler | Function | Default 429 Handler | Customizes the HTTP response returned when a request exceeds the configured limit. | | standardHeaders | boolean | true | Enables or disables injection of RFC 9333 rate limit headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset). |

🧠 The 5 Algorithms (Deep Dive)

Choosing the right algorithm is critical for your specific endpoint's health.

  1. Fixed Window (fixed-window) The simplest and most memory-efficient. Groups requests into rigid blocks of time (e.g., exactly 12:00:00 to 12:01:00).

Pros: Extremely fast, low memory footprint.

Cons: Susceptible to the "Edge Burst" problem. A user can send 100 requests at 12:00:59, and another 100 at 12:01:01, effectively pushing 200 requests in 2 seconds.

  1. Sliding Window Counter (sliding-counter) The best all-rounder. Smoothes out traffic spikes by calculating an overlap percentage between the previous time window and the current one.

Pros: Highly memory efficient. Fixes the Edge Burst problem.

Cons: Assumes a perfectly uniform distribution of requests in the previous window.

Best for: Standard REST API limits (e.g., 100 req/min on general endpoints).

  1. Sliding Window Log (sliding-log) The most mathematically precise limit. Logs the exact timestamp of every single request. Our implementation uses O(k) array slicing optimizations for high performance.

Pros: 100% accurate rate limiting down to the millisecond.

Cons: Expensive memory footprint. Storing 10,000 timestamps for an active IP takes up RAM/Redis space.

Best for: Extremely strict endpoints like /sms/send or /payment/process.

  1. Token Bucket (token-bucket) Uses floating-point precision to slowly add tokens to a bucket over time. Requests remove tokens.

Pros: Allows for immediate, massive bursts of traffic up to the limit, but forces a steady downshift afterward.

Cons: Slightly more complex to tune refill rates.

Best for: Third-party Webhook ingestion or Public APIs where clients batch requests.

  1. Leaky Bucket (leaky-bucket) Requests are dumped into a bucket. The bucket leaks them into the server at a perfectly strict, continuous floating-point rate.

Pros: Guarantees your downstream database will never experience a burst spike. Forces extreme traffic smoothing.

Cons: Users experiencing spikes will immediately get 429s instead of processing quickly.

Best for: Protecting fragile legacy systems or heavy background-worker queues.

🚔 The Penalty Box (WAF Abuse Prevention)

Normal rate limiters tell abusers to "slow down." The Penalty Box treats them as malicious actors.

If a user repeatedly ignores your 429 Too Many Requests responses, they are placed in the Penalty Box. While in the box, their requests do not even touch the algorithms; they are instantly rejected with a 403 Forbidden.

Example

const secureLimiter = createLimiter({
    limit: 50,
    windowMs: 60000,
    penaltyBox: {
        enabled: true,
        maxViolations: 3,           // Hitting 429 three times triggers the box
        jailTimeMs: 60 * 60 * 1000  // Jail time: 1 Hour
    }
});

🎭 Dynamic Tiering & Cost-Based Limiting

You can pass asynchronous functions to configuration properties to evaluate limits on the fly based on the request context.

1. Subscription Tiers

const tierLimiter = createLimiter({
    algorithm: 'sliding-counter',
    windowMs: 60000,
    limit: async (req) => {
        if (!req.user) return 10;                      // Guest: 10/min
        if (req.user.plan === 'premium') return 1000;  // Premium: 1000/min
        return 100;                                    // Free: 100/min
    }
});

2. Expensive Route Costs

Protect CPU-heavy routes from draining your server.

const heavyLimiter = createLimiter({
    algorithm: 'token-bucket',
    limit: 100,
    windowMs: 60000,
    cost: async (req) => {
        // Generating a PDF consumes 50 tokens instantly
        if (req.path === '/export-pdf') return 50;

        // Normal requests consume 1 token
        return 1;
    }
});

📡 Event Telemetry & Monitoring

Connect express-multi-limiter directly to your SIEM, Datadog, or Winston loggers using the exported telemetry EventEmitter.

const { telemetry } = require('express-multi-limiter');

// 🟢 Fired on every successful request passing through
telemetry.on('allowed', ({ key, route, cost, remaining }) => {
    // E.g., send metrics to Datadog
});

// 🟡 Fired when a standard rate-limit (429) is hit
telemetry.on('limited', ({ key, route, retryAfterSeconds }) => {
    console.log(`User ${key} rate limited at ${route}.`);
});

// 🔴 Fired the exact moment an abuser is sent to the Penalty Box
telemetry.on('jailed', ({ key }) => {
    console.warn(`🚨 SECURITY ALERT: IP ${key} was sent to jail!`);
});

// 🔴 Fired when a jailed user or a Blocklisted IP attempts to connect
telemetry.on('blocked', ({ key, reason, route }) => {
    console.log(`🛡️ Blocked ${key} (${reason}) at ${route}`);
});

// 💥 Fired if the Redis/Database store throws an error
telemetry.on('error', (error) => {
    console.error(`💥 Limiter DB Error:`, error.message);
});

🌐 RFC 9333 Standard Headers

By default, the middleware injects modern Internet Engineering Task Force (IETF) standard headers alongside legacy headers:

  • RateLimit-Limit: The total request allowance within the window.
  • RateLimit-Remaining: The number of requests remaining.
  • RateLimit-Reset: The exact number of seconds until the limit resets.

Note: Legacy headers like X-RateLimit-Limit are also included for backward compatibility with older API clients.


🏰 Architecture: Solving Race Conditions (Mutex)

A common flaw in Node.js rate limiters running purely in memory is concurrency failure. Because standard implementations rely on async/await flows:

  1. Request A reads count: 5. Event loop yields.
  2. Request B reads count: 5. Event loop yields.
  3. Request A writes count: 6.
  4. Request B writes count: 6. (A request was lost!)

The Mutex Solution

Our MemoryStore implements a strictly ordered Promise-chaining Mutex queue.

const previous = this.locks.get(key) || Promise.resolve();

let release;

const mine = new Promise(resolve => {
    release = resolve;
});

const queued = previous.then(() => mine);

this.locks.set(key, queued);

Every incoming request for a specific IP is chained to the resolution of the previous request's critical section. This guarantees 100% chronological state safety, preventing abusers from bypassing limits using concurrent blast attacks.