express-rate-limit-redis-slim
v1.0.1
Published
A lightweight, high-performance Redis store for express-rate-limit with atomic Lua scripting
Maintainers
Readme
express-rate-limit-redis-slim
A lightweight, high-performance Redis store for express-rate-limit with atomic Lua scripting.
Why This Package?
🚀 Truly "Slim"
Unlike alternatives, this package has zero bloat:
- Pure TypeScript/ESM with CJS fallback
- No legacy CommonJS wrappers
- Minimal dependencies
⚡ Atomic Operations
Traditional Redis rate limiters use separate INCR and EXPIRE commands, creating race conditions:
Request A: INCR key → 1
Request B: INCR key → 2
Request A: EXPIRE key 60 ← What if this fails?
Request B: EXPIRE key 60Our approach uses a single Lua script that executes atomically:
local count = redis.call('INCR', key)
if count == 1 then
redis.call('EXPIRE', key, window)
end
return { count, ttl }This ensures increment and expiry happen in the exact same transaction, preventing "zombie keys" that never expire.
🔌 Flexible Client Support
Works with any Redis client that implements eval():
- ✅ ioredis (recommended)
- ✅ @upstash/redis (perfect for serverless)
- ✅ Custom clients via
sendCommand
Installation
npm install express-rate-limit-redis-slim express-rate-limit
# With ioredis (recommended)
npm install ioredis
# Or with Upstash for serverless
npm install @upstash/redisQuick Start
With ioredis
import express from 'express';
import { rateLimit } from 'express-rate-limit';
import { RedisStore } from 'express-rate-limit-redis-slim';
import Redis from 'ioredis';
const app = express();
// Create Redis client
const redisClient = new Redis({
host: 'localhost',
port: 6379,
});
// Create rate limiter with Redis store
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 100, // 100 requests per window
standardHeaders: 'draft-7',
legacyHeaders: false,
store: new RedisStore({
client: redisClient,
prefix: 'rl:', // Optional, defaults to 'rl:'
}),
});
app.use(limiter);
app.get('/', (req, res) => {
res.send('Hello, world!');
});
app.listen(3000);With @upstash/redis (Serverless)
import { rateLimit } from 'express-rate-limit';
import { RedisStore } from 'express-rate-limit-redis-slim';
import { Redis } from '@upstash/redis';
const upstashRedis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
limit: 10,
store: new RedisStore({
sendCommand: async (command, args) => {
if (command === 'EVAL') {
const [script, numKeys, ...rest] = args;
return upstashRedis.eval(script as string, rest.slice(0, numKeys as number), rest.slice(numKeys as number));
}
throw new Error(`Unsupported command: ${command}`);
},
}),
});With Custom sendCommand
import { RedisStore } from 'express-rate-limit-redis-slim';
const store = new RedisStore({
sendCommand: async (command, args) => {
// Your custom Redis implementation
return await myRedisClient.execute(command, ...args);
},
});Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| client | RedisClient | - | A Redis client instance with eval() method |
| sendCommand | SendCommandFn | - | Alternative: custom function to send Redis commands |
| prefix | string | "rl:" | Prefix for all rate limit keys in Redis |
| passOnConnectionError | boolean | false | If true, allow requests when Redis is unavailable |
Note: You must provide either
clientorsendCommand, but not both.
API Reference
RedisStore
Constructor
const store = new RedisStore(options: RedisStoreOptions);Methods
| Method | Description |
|--------|-------------|
| init(options) | Called automatically by express-rate-limit |
| increment(key) | Increment hit count for a key. Returns { totalHits, resetTime } |
| decrement(key) | Decrement hit count (for when a request shouldn't count) |
| resetKey(key) | Clear rate limit for a specific key |
| shutdown() | Cleanup (no-op, as client lifecycle is external) |
Error Handling
Graceful Degradation
When Redis is unavailable, you can choose to:
- Block requests (default): Throws an error, which express-rate-limit handles
- Allow requests: Set
passOnConnectionError: true
const store = new RedisStore({
client: redisClient,
passOnConnectionError: true, // Allow requests if Redis fails
});Connection Events
Since you own the Redis client, handle connection events yourself:
const redisClient = new Redis();
redisClient.on('error', (err) => {
console.error('Redis connection error:', err);
});
redisClient.on('connect', () => {
console.log('Connected to Redis');
});Performance
This library is optimized for high throughput:
| Benchmark | Operations/sec | |-----------|----------------| | Single key increment | ~50,000/s | | Multi-key concurrent | ~30,000/s |
Tested on Redis 7.0, Node.js 20, local connection
The atomic Lua script executes in a single round-trip, minimizing latency even with remote Redis instances.
TypeScript Support
Full TypeScript support with exported types:
import {
RedisStore,
RedisStoreOptions,
RedisClient,
SendCommandFn,
IncrementResponse,
} from 'express-rate-limit-redis-slim';Contributing
Contributions are welcome! Please read our contributing guidelines before submitting a PR.
# Clone the repo
git clone https://github.com/yourusername/express-rate-limit-redis-slim.git
# Install dependencies
npm install
# Run tests
npm test
# Build
npm run buildLicense
MIT © 2025
