redis-distributed-rate-limiter
v1.3.0
Published
High-performance distributed rate-limiting middleware for Express using atomic Redis Lua scripting and non-blocking telemetry hooks.
Maintainers
Readme
redis-distributed-rate-limiter
A high-performance, ultra-low-latency distributed rate-limiting middleware for Express.js applications. Built with atomic Redis Lua scripting to prevent race conditions across horizontally scaled infrastructure, featuring pluggable rate-limiting algorithms and a decoupled asynchronous telemetry hook for real-time traffic observability.
🏗️ Core Architecture
This library isolates the critical path of API request evaluation from the secondary collection of analytical telemetry data. All three supported algorithms execute atomically within a single Redis Lua script per request, guaranteeing thread safety across any number of horizontally scaled app instances sharing the same Redis backend.
- Distributed Synchronization: State is centralized in Redis, allowing multiple stateless API gateway instances to scale out horizontally while maintaining shared rate-limit quotas.
- Pluggable Algorithms: Choose the counting strategy that fits your traffic shape — exact logging, constant-memory counting, or burst-friendly token bucketing — via a single config flag.
- Atomic Execution: Every algorithm is implemented as a single Lua script (
EVAL), so the read-check-write cycle is race-condition-free even under concurrent same-millisecond traffic. - Non-Blocking Telemetry Pipelines: Exposes a clean event-driven interface that offloads log recording out of the main request-response lifecycle loop using
process.nextTick(). - Resilient Fail-Open Design: Automatically logs internal exceptions and fails open, preventing cache cluster outages from crashing customer-facing API nodes.
- Spoof-Resistant IP Resolution: Right-to-left
X-Forwarded-Forparsing against a configurable trusted-proxy allowlist (with CIDR support), so attackers can't bypass limits by forging headers.
⚙️ Installation
Install the package via the npm registry:
npm install redis-distributed-rate-limiter
Peer Dependencies
Ensure you have the official Redis client installed and running in your root application environment:
npm install redis
🚀 Quick Start (TypeScript / JavaScript)
Initialize the middleware by passing your pre-configured Redis client wrapper and setting up your threshold definitions:
import express from 'express';
import { createClient } from 'redis';
import { distributedRateLimiter, TelemetryLog } from 'redis-distributed-rate-limiter';
const app = express();
// 1. Initialize your centralized Redis client connection
const redisClient = createClient({ url: 'redis://localhost:6379' });
redisClient.connect().then(() => console.log('Redis connected successfully'));
// 2. Configure the Distributed Rate Limiter Middleware
const limiter = distributedRateLimiter({
redisClient: redisClient,
algorithm: 'SLIDING_LOG', // optional - see "Choosing an Algorithm" below. Defaults to 'SLIDING_LOG'.
windowInMs: 60000, // 1 Minute moving sliding window
maxRequests: 10, // Allow up to 10 requests per window
// Custom non-blocking callback hook for async logging infrastructure
onLog: (logData: TelemetryLog) => {
// Example: Stream log payloads asynchronously to an Apache Kafka Producer,
// a microservice logger, or push to an analytics queue.
console.log(`[Telemetry Ingress] IP: ${logData.ip} | Status: ${logData.status} | Hits: ${logData.currentCount}`);
}
});
// 3. Apply the middleware cluster globally or to explicit routes
app.use(limiter);
app.get('/api/v1/resource', (req, res) => {
res.json({ success: true, message: "Welcome to the secure gateway." });
});
// Note: If running behind reverse proxies (AWS ALB, Nginx, Cloudflare, Vercel)
app.set('trust proxy', true);
app.listen(3000, () => console.log('API Gateway active on port 3000'));
🧮 Choosing an Algorithm
As of v1.3.0, algorithm is an optional config flag letting you pick the counting strategy per limiter instance. If omitted, it defaults to 'SLIDING_LOG' — existing installs upgrading from earlier versions behave identically with no config changes required.
const limiter = distributedRateLimiter({
redisClient,
algorithm: 'SLIDING_COUNTER', // 'SLIDING_LOG' | 'SLIDING_COUNTER' | 'TOKEN_BUCKET'
windowInMs: 60000,
maxRequests: 100,
});windowInMs and maxRequests keep the same meaning across all three algorithms, so you can switch strategies without restructuring your config: for TOKEN_BUCKET, maxRequests becomes the bucket's max capacity and windowInMs is the time to fully refill an empty bucket (i.e. refillRate = maxRequests / windowInMs).
| Algorithm | Memory / Client | Precision | Best For |
| --- | --- | --- | --- |
| SLIDING_LOG (default) | O(N) — one entry per request in-window | Exact | Low/medium traffic, or when hard precision matters (billing, abuse-critical endpoints) |
| SLIDING_COUNTER | O(1) — 3 fixed fields | Approximate (weighted estimate across the window boundary) | High-traffic services where Redis memory footprint matters more than perfect precision |
| TOKEN_BUCKET | O(1) — 2 fixed fields | Exact, burst-aware | Public developer APIs where legitimate clients need to burst (e.g. batch operations) without being punished, while still capping sustained average rate |
A note on SLIDING_COUNTER's tradeoff: it estimates traffic across the previous/current window boundary using a linear weighting formula rather than tracking exact timestamps. This is the standard, well-understood tradeoff of this algorithm (the same approach used in Cloudflare's public rate-limiting design) — it keeps memory flat regardless of request volume, but under a sharp traffic spike right at a window boundary it can permit a short burst modestly above maxRequests. If your endpoint needs a hard, unconditional ceiling, use SLIDING_LOG or TOKEN_BUCKET instead.
🎛️ Configuration Reference
The distributedRateLimiter initialization constructor accepts the following structured parameters:
| Property | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| redisClient | any | Yes | — | An open, active v4 instance connection to your Redis deployment server. |
| windowInMs | number | Yes | — | Moving time framework window tracked in milliseconds (e.g., 60000 for 1 minute). For TOKEN_BUCKET, this is the full-refill time. |
| maxRequests | number | Yes | — | Total allowed operations inside the specific time frame constraint bounds. For TOKEN_BUCKET, this is the bucket capacity. |
| algorithm | 'SLIDING_LOG' \| 'SLIDING_COUNTER' \| 'TOKEN_BUCKET' | No | 'SLIDING_LOG' | Selects the counting algorithm. See Choosing an Algorithm. |
| trustedProxies | string[] | No | [] | List of trusted proxy IPs or CIDR subnets (e.g., ['127.0.0.1', '10.0.0.0/8']). Essential for blocking IP header spoofing behind Nginx, AWS, or Cloudflare. |
| limitIPv6Subnet | boolean | No | false | Toggles whether to mask native IPv6 range streams to prevent subnet address rotation exploits. |
| ipv6Subnet | number | No | 56 | Bitmask routing depth constraints used if limitIPv6Subnet is enabled (replicates express-rate-limit defaults). |
| keyGenerator | (req) => string | No | IP-based | Overrides default key tracking logic. Allows tracking limits using authorization tokens, API keys, or User IDs instead of plain IP routing. |
| onLog | (log: TelemetryLog) => void | No | — | Callback hook firing asynchronously upon execution completion. Passes complete log payload packages. |
🔒 Securing behind Reverse Proxies (Nginx, Cloudflare, AWS ALB)
If your application sits behind a load balancer or proxy layer, provide your proxy IP blocks to the trustedProxies parameter array. This forces the middleware to safely evaluate the network traversal chain from right-to-left, preventing clients from spoofing their location via artificial X-Forwarded-For entries.
import { distributedRateLimiter } from 'redis-distributed-rate-limiter';
const limiter = distributedRateLimiter({
redisClient: myRedisInstance,
windowInMs: 60000,
maxRequests: 100,
// Define your trusted infrastructure boundaries
trustedProxies: ['127.0.0.1', '10.0.0.0/8'],
limitIPv6Subnet: true
});⚠️ If
trustedProxiesis left empty while anX-Forwarded-Forheader is present, the middleware logs a warning and falls back to the raw TCP socket address, ignoring the header entirely. This is intentional — a misconfigured trust boundary is a bigger risk than an ignored header.
📊 Telemetry Log Payload Schema
The onLog event emitter passes an immutable TelemetryLog object containing downstream performance values:
interface TelemetryLog {
ip: string; // Remote user origin IP or mapped proxy client
path: string; // Visited API node endpoint URL string
method: string; // Executed HTTP request method verb (GET, POST, etc.)
status: 'ALLOWED' | 'BLOCKED'; // Resolution status string output
timestamp: number; // POSIX timestamp tracking exact request ingress time
currentCount: number; // Requests consumed so far in the current window/bucket, across all algorithms
}
🛡️ Response Headers
Successful operations return standard rate limit metadata embedded securely inside response headers, regardless of which algorithm is active:
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 9
X-RateLimit-Reset: 2026-07-05T11:04:48.201Z
When thresholds are broken, the middleware automatically rejects traffic with an explicit 429 Too Many Requests status payload:
{
"status": 429,
"error": "Too Many Requests",
"message": "Rate limit exceeded. Please try again in 60 seconds."
}
🧪 Testing
This package ships with 22 Jest/Supertest integration tests covering:
- Standard threshold enforcement and 429 responses
- Dual-stack (IPv4-mapped IPv6) normalization
- IPv6 subnet-rotation abuse prevention
- Proxy-spoofing rejection and CIDR-based trust validation
- Multi-hop trusted proxy chains
- Fail-open resilience on Redis outages
- Custom
keyGeneratoroverrides - All three algorithms' counting correctness, including window-boundary weighting math (
SLIDING_COUNTER) and burst/refill math (TOKEN_BUCKET), verified against a live Redis instance
Run them with:
npm run test