@srinidhi-kulkarni/rate-limiter
v1.0.3
Published
Distributed Redis-backed Token Bucket Rate Limiter for Node.js and Express
Downloads
47
Maintainers
Readme
@srinidhi/rate-limiter
A high-performance distributed Token Bucket Rate Limiter for Node.js and Express powered by Redis and Lua scripting.
Designed for applications that require atomic, distributed, and race-condition-free rate limiting across multiple server instances.
Why this library?
Traditional in-memory rate limiters work well for a single application instance but become inconsistent when applications are deployed across multiple servers.
This library stores rate limiting state in Redis and executes the complete Token Bucket algorithm using an atomic Redis Lua script, ensuring every application instance shares the same bucket state without race conditions.
Features
- 🚀 Distributed rate limiting using Redis
- ⚡ Atomic Token Bucket implementation using Lua
- 🔒 Race-condition free
- 🌐 Works across multiple application instances
- 🧩 Express middleware support
- 🎯 Per-client configurable limits
- 📦 Written in TypeScript
- 🔥 Supports burst traffic
- ♻️ Automatic token refill
- ✅ Production-ready architecture
Installation
npm install @srinidhi/rate-limiterPrerequisites
A running Redis server.
Example:
redis://localhost:6379Quick Start
import express from "express";
import { RateLimiter } from "@srinidhi/rate-limiter";
const app = express();
const limiter = new RateLimiter({
redisUrl: "redis://localhost:6379",
});
await limiter.connect();
await limiter.registerClient({
clientId: "demo-client",
capacity: 10,
refillRate: 5,
});
app.use(
limiter.middleware({
clientId: (req) =>
req.headers["x-api-key"] as string,
})
);
app.get("/", (_, res) => {
res.json({
message: "Hello World",
});
});
app.listen(3000);How It Works
The library implements the Token Bucket algorithm.
Each client is configured with:
- Capacity – Maximum number of tokens that can be stored.
- Refill Rate – Number of tokens regenerated per second.
For every incoming request, the library:
- Loads the client's bucket from Redis.
- Executes the Token Bucket algorithm atomically using Lua.
- Refills tokens based on elapsed time.
- Consumes a token if available.
- Returns whether the request is allowed.
- Stores the updated bucket back in Redis.
Since the entire operation is executed as a single Redis Lua script, concurrent requests cannot corrupt bucket state.
Registering Clients
Each client can have its own rate limiting configuration.
await limiter.registerClient({
clientId: "free-user",
capacity: 10,
refillRate: 5,
});Example for another plan:
await limiter.registerClient({
clientId: "pro-user",
capacity: 100,
refillRate: 50,
});Express Middleware
The middleware accepts a callback that extracts a unique identifier for each request.
This identifier can be:
- API Key
- User ID
- JWT Subject
- IP Address
- Tenant ID
- Any unique client identifier
API Key Example
app.use(
limiter.middleware({
clientId: (req) =>
req.headers["x-api-key"] as string,
})
);JWT Example
app.use(
limiter.middleware({
clientId: (req) => req.user.id,
})
);IP Address Example
app.use(
limiter.middleware({
clientId: (req) => req.ip,
})
);API
Create Rate Limiter
const limiter = new RateLimiter({
redisUrl: "redis://localhost:6379",
});connect()
Initializes the Redis connection and loads the Lua script.
await limiter.connect();registerClient()
Registers a new client.
await limiter.registerClient({
clientId: "client-1",
capacity: 20,
refillRate: 10,
});updateClient()
Updates an existing client.
await limiter.updateClient({
clientId: "client-1",
capacity: 50,
refillRate: 20,
});deleteClient()
Deletes the client configuration and bucket.
await limiter.deleteClient("client-1");check()
Checks whether a request is allowed.
const result = await limiter.check("client-1");Returns:
{
allowed: true,
remainingTokens: 8,
resetTime: 1740000000000
}middleware()
Creates Express middleware.
app.use(
limiter.middleware({
clientId: (req) =>
req.headers["x-api-key"] as string,
})
);HTTP Response Headers
The middleware automatically adds the following headers.
| Header | Description | |---------|-------------| | X-RateLimit-Limit | Maximum bucket capacity | | X-RateLimit-Remaining | Remaining tokens | | X-RateLimit-Reset | Timestamp when another token becomes available |
Example
Configuration
Capacity = 10
Refill Rate = 5 tokens/secBurst of 15 requests
Requests 1–10
✅ AllowedRequests 11–15
❌ HTTP 429 Too Many RequestsAfter waiting 2 seconds:
5 tokens/sec
↓
10 tokens regenerated
↓
Requests are allowed againError Responses
| Status | Description | |---------|-------------| | 400 | Missing client identifier | | 404 | Client not registered | | 429 | Rate limit exceeded |
Performance
Stress tested using Autocannon.
Example results:
- ~3,000 Requests/sec
- Correct burst handling
- Correct refill behaviour
- Atomic under concurrency
- Redis-backed distributed state
- No race conditions
Why Redis + Lua?
A traditional Redis implementation typically performs multiple operations:
GET
↓
Modify
↓
SETUnder concurrent requests, multiple clients can read the same bucket before it is updated, leading to race conditions.
This library executes the complete Token Bucket algorithm inside a single Redis Lua script.
Benefits:
- Atomic execution
- No race conditions
- No distributed locks
- High throughput
- Consistent behaviour across multiple application instances
Requirements
- Node.js 18+
- Redis 6+
- TypeScript 5+
License
MIT
Author
Srinidhi Kulkarni
GitHub: https://github.com/Srinidhi444
