rate-limiter-flexible
Version:
Node.js atomic and non-atomic counters, rate limiting tools, protection from DoS and brute-force attacks at scale
480 lines (378 loc) • 18.3 kB
Markdown
# rate-limiter-flexible
Comprehensive reference for the `rate-limiter-flexible` npm package — atomic and non-atomic counters and rate limiting tools for Node.js, Deno, and browsers (Memory limiter).
Harden application security against brute-force and DoS attacks.
Sources: [README](https://github.com/animir/node-rate-limiter-flexible) · [Wiki](https://github.com/animir/node-rate-limiter-flexible/wiki)
## Installation & Import
```bash
npm i --save rate-limiter-flexible
# or
yarn add rate-limiter-flexible
```
```js
// ESM (recommended)
import { RateLimiterMemory } from "rate-limiter-flexible";
// Direct import (tree-shakeable)
import RateLimiterMemory from "rate-limiter-flexible/lib/RateLimiterMemory.js";
// CommonJS
const { RateLimiterMemory } = require("rate-limiter-flexible");
```
**TypeScript:** Types are bundled in `types.d.ts`. Since v8.0.0, `RateLimiterQueueError` must be imported from defaults: `import { RateLimiterQueueError } from "rate-limiter-flexible"`.
## Core Concepts
- **Atomic increments** — all operations use atomic increments to prevent race conditions.
- **Enhanced fixed window** algorithm — starts counting from the moment a request is received, diversifying rate limit reset times across clients. Much faster than rolling window. See [comparative benchmarks](https://github.com/animir/node-rate-limiter-flexible/wiki/Comparative-benchmarks).
- **Zero production dependencies.**
- **Deno compatible** — see [example gist](https://gist.github.com/animir/d06ca92931677f330d3f2d4c6c3108e4).
- **Browser compatible** — `RateLimiterMemory` works in the browser.
- **Unified API** — all limiters (Memory, Redis, Mongo, etc.) share the same interface. Pick a store, configure options, consume points by key (IP, user ID, token, route, any string).
- Speed up any store limiter with `inMemoryBlockOnConsumed` option.
## Options Reference
Options can be changed at runtime: `rateLimiter.points = 50`, `rateLimiter.duration = 5`.
### Common Options
| Option | Default | Description |
|--------|---------|-------------|
| `points` | **Required** | Max points consumable over `duration`. Must be a number. |
| `duration` | **Required** | Seconds before points reset (from first consume). Must be >= 0. `0` = never expire. |
| `keyPrefix` | `'rlflx'` | Unique prefix per limiter to avoid key collisions. For some stores, used as table/collection name. |
| `blockDuration` | `0` | If >0, block key for this many seconds once points exhausted. |
| `storeClient` | — | Required for store limiters. Accepts `@valkey/valkey-glide`, `iovalkey`, `redis`, `ioredis`, `memcached`, `mongodb`, `pg`, `mysql2`, `mysql`, Sequelize, TypeORM, Knex, or any related pool/connection. |
| `inMemoryBlockOnConsumed` | `0` | Block key in process memory after this many points consumed (DoS protection). Should be >= `points`. |
| `inMemoryBlockDuration` | `0` | Seconds to block in memory. Set same as `blockDuration` for consistency across processes. |
| `insuranceLimiter` | `undefined` | Fallback limiter instance used when store errors occur. |
| `execEvenly` | `false` | Delay actions evenly over duration (Leaky Bucket pattern). |
| `execEvenlyMinDelayMs` | `duration * 1000 / points` | Minimum delay when `execEvenly` is true. |
| `clearExpiredByTimeout` | `true` | (MySQL, SQLite, PostgreSQL) Auto-delete expired data every 5 min. |
| `tableCreated` | `false` | (MySQL, PostgreSQL, SQLite, DynamoDB) Skip table creation if already exists. |
| `tableName` | `keyPrefix` | (MongoDB, MySQL, PostgreSQL, SQLite) Custom table/collection name. |
| `dbName` | varies | Database name (MySQL: `'rtlmtrflx'`, MongoDB: `'node-rate-limiter-flexible'`). |
### Store-Specific Options
**Redis:**
- `rejectIfRedisNotReady` (default `false`) — reject immediately when Redis not ready.
- `customIncrTtlLuaScript` — custom Lua script for increments.
- `useRedisPackage` (default `false`) — set `true` for `redis` package v4+.
- `useRedis3AndLowerPackage` (default `false`) — for `redis` package v3 or lower (not fully supported).
**MongoDB:**
- `indexKeyPrefix` (default `{}`) — combined index attributes.
- `disableIndexesCreation` (default `false`) — disable auto-index creation; call `await limiter.createIndexes()` manually.
**PostgreSQL:**
- `schemaName` — custom schema (default: `public`).
**DynamoDB:**
- `dynamoTableOpts` (default `{readCapacityUnits: 25, writeCapacityUnits: 25}`).
- `ttlSet` (default `false`) — skip TTL check on instantiation (useful for serverless).
**Drizzle:**
- `schema` — required pgTable definition.
**Cluster:**
- `timeoutMs` (default `5000`) — IPC timeout between worker and master.
**Storedb type:**
- `storeType` — for Knex set to `'knex'`; for SQLite set to `'better-sqlite3'` or `'knex'`.
## API Methods
All methods return Promises.
| Method | Description |
|--------|-------------|
| `consume(key, points = 1, options = {})` | Consume points. Resolves with `RateLimiterRes`, rejects when limit reached. |
| `get(key)` | Get current `RateLimiterRes` for key without consuming. Returns `null` if no record. |
| `set(key, points, secDuration)` | Set consumed points and duration for a key. |
| `block(key, secDuration)` | Block key for `secDuration` seconds. |
| `delete(key)` | Delete key and return `true`/`false`. |
| `deleteInMemoryBlockedAll()` | Clear all in-memory blocked keys. |
| `penalty(key, points = 1)` | Add penalty points (same as consume but semantic). |
| `reward(key, points = 1)` | Subtract points (reward good behavior). |
| `getKey(key)` | Get internal key with prefix applied. |
### RateLimiterRes Object
| Property | Description |
|----------|-------------|
| `msBeforeNext` | Milliseconds before points reset. |
| `remainingPoints` | Points remaining in current duration. |
| `consumedPoints` | Points already consumed. |
| `isFirstInDuration` | Whether this is the first action in current duration. |
### HTTP Response Headers Pattern
```js
const headers = {
"Retry-After": rateLimiterRes.msBeforeNext / 1000,
"X-RateLimit-Limit": opts.points,
"X-RateLimit-Remaining": rateLimiterRes.remainingPoints,
"X-RateLimit-Reset": Math.ceil((Date.now() + rateLimiterRes.msBeforeNext) / 1000)
};
```
## Available Limiters
### Store-based Limiters
| Limiter | Store | Notes |
|---------|-------|-------|
| `RateLimiterRedis` | Redis >=2.6.12 | Works with `ioredis` by default. Requires `+@read +@write +EVAL +EVALSHA` permissions. `RateLimiterRedisNonAtomic` also available (faster, race conditions possible). |
| `RateLimiterMongo` | MongoDB | Supports sharding. |
| `RateLimiterMySQL` | MySQL | Via `mysql2`, `mysql`, Sequelize, or Knex. |
| `RateLimiterPostgres` | PostgreSQL | Via `pg`, Sequelize, TypeORM, or Knex. Custom schema support. |
| `RateLimiterMemcache` | Memcached | |
| `RateLimiterDynamo` | DynamoDB | |
| `RateLimiterPrisma` | via Prisma ORM | |
| `RateLimiterDrizzle` | via Drizzle ORM | Atomic and non-atomic. |
| `RateLimiterSQLite` | SQLite | Via `sqlite3`, `better-sqlite3`, or Knex. |
| `RateLimiterEtcd` | etcd | Atomic and non-atomic. |
| `RateLimiterIoValkey` | Valkey (iovalkey) | |
| `RateLimiterValkeyGlide` | Valkey Glide | |
### In-Process Limiters
| Limiter | Notes |
|---------|-------|
| `RateLimiterMemory` | Single-process only. Fastest. Also works in the browser. Max duration/blockDuration ~24 days (2147483 sec) due to `setTimeout` limitation. |
| `RateLimiterCluster` | Node.js cluster (IPC to master). |
| `RateLimiterClusterMasterPM2` | PM2 cluster mode. |
### Composite / Wrapper Limiters
| Limiter | Description |
|---------|-------------|
| `BurstyRateLimiter` | Combines two limiters: primary + burst allowance. |
| `RateLimiterUnion` | Consume from multiple limiters simultaneously. Only `consume` method. Accepts any `RateLimiterAbstract` or `RateLimiterCompatibleAbstract` instance. |
| `RateLimiterQueue` | Queue actions and execute at controlled rate (FIFO). |
| `RLWrapperBlackAndWhite` | Wrap any limiter with black/white IP lists. Can be used as `insuranceLimiter`, in `RLWrapperTimeouts`, or `RateLimiterUnion`. |
| `RLWrapperTimeouts` | Wrap any limiter with custom timeout behavior. |
## Common Patterns
### Express Middleware
```js
const Redis = require('ioredis');
const { RateLimiterRedis } = require('rate-limiter-flexible');
const redisClient = new Redis({ enableOfflineQueue: false });
const rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'middleware',
points: 10,
duration: 1,
});
const rateLimiterMiddleware = (req, res, next) => {
rateLimiter.consume(req.ip)
.then(() => next())
.catch(() => res.status(429).send('Too Many Requests'));
};
app.use(rateLimiterMiddleware);
```
### Koa Middleware
```js
app.use(async (ctx, next) => {
try {
await rateLimiter.consume(ctx.ip);
} catch (rejRes) {
ctx.status = 429;
ctx.body = 'Too Many Requests';
return;
}
await next();
});
```
### Hapi Plugin
```js
server.ext('onPreAuth', async (request, h) => {
try {
await rateLimiter.consume(request.info.remoteAddress);
return h.continue;
} catch (rej) {
if (rej instanceof Error) {
return Boom.internal('Try later');
}
const error = Boom.tooManyRequests('Rate limit exceeded');
error.output.headers['Retry-After'] = Math.round(rej.msBeforeNext / 1000) || 1;
throw error;
}
});
```
### Login Brute-Force Protection (Minimal)
Two limiters: one for consecutive fails by username, one for fails per IP per day.
```js
const limiterSlowBruteByIP = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'login_fail_ip_per_day',
points: 100,
duration: 60 * 60 * 24,
blockDuration: 60 * 60 * 24,
});
const limiterConsecutiveFailsByUsernameAndIP = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'login_fail_consecutive_username_and_ip',
points: 10,
duration: 60 * 60 * 24 * 90,
blockDuration: 60 * 60,
});
```
Key pattern: `get()` first (cheap read), then `consume()` only on failure.
Reset on successful login: `await limiter.delete(key)`.
### Dynamic Block Duration (Fibonacci escalation)
```js
const loginLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'login',
points: 5,
duration: 15 * 60,
});
const limiterConsecutiveOutOfLimits = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'login_consecutive_outoflimits',
points: 99999,
duration: 0, // never expire — acts as counter
});
// On limit reached: penalty() on counter, then block() with Fibonacci minutes
```
### Authorized vs Unauthorized Users
```js
const rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
points: 300,
duration: 60,
});
// Consume 1 point for authenticated, 30 for anonymous
const key = req.userId ? req.userId : req.ip;
const pointsToConsume = req.userId ? 1 : 30;
rateLimiter.consume(key, pointsToConsume);
```
### Websocket Flood Protection
```js
const rateLimiter = new RateLimiterMemory({
points: 5,
duration: 1,
});
io.on('connection', (socket) => {
socket.on('bcast', async (data) => {
try {
await rateLimiter.consume(socket.handshake.address);
socket.emit('news', { data });
socket.broadcast.emit('news', { data });
} catch (rejRes) {
socket.emit('blocked', { 'retry-ms': rejRes.msBeforeNext });
}
});
});
```
### BurstyRateLimiter
```js
const burstyLimiter = new BurstyRateLimiter(
new RateLimiterMemory({ points: 2, duration: 1 }),
new RateLimiterMemory({ keyPrefix: 'burst', points: 5, duration: 10 })
);
// Allows 2/sec normally + burst of 5 per 10 sec
```
### RateLimiterUnion (multiple simultaneous limits)
```js
const limiter1 = new RateLimiterMemory({ keyPrefix: 'limit1', points: 1, duration: 1 });
const limiter2 = new RateLimiterMemory({ keyPrefix: 'limit2', points: 5, duration: 60 });
const union = new RateLimiterUnion(limiter1, limiter2);
// Rejects if ANY limiter is exhausted
```
### RateLimiterQueue (FIFO execution)
```js
const limiterFlexible = new RateLimiterMemory({ points: 2, duration: 1 });
const limiterQueue = new RateLimiterQueue(limiterFlexible, { maxQueueSize: 100 });
// Queued actions execute at limiter's rate
await limiterQueue.removeTokens(1);
```
### Insurance Strategy (fallback on store failure)
```js
const rateLimiterMemory = new RateLimiterMemory({ points: 1, duration: 1 });
const rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
points: 5,
duration: 1,
insuranceLimiter: rateLimiterMemory,
});
// If Redis fails, RateLimiterMemory is used automatically
```
Note: insurance limiter automatically inherits `blockDuration` and `execEvenly` from parent. Data is NOT copied between limiters when main store recovers.
### In-Memory Block Strategy (DoS mitigation)
```js
const rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
points: 5,
duration: 1,
inMemoryBlockOnConsumed: 5, // Block in memory after 5 points consumed
// inMemoryBlockDuration: 10, // Optional: block for fixed duration
});
// ~7x faster: blocked keys served from memory, no store requests
```
Works for `consume()` only. Keys auto-expire without `setTimeout` (no Event Loop overhead).
### Black and White Lists
```js
const limiterWrapped = new RLWrapperBlackAndWhite({
limiter: rateLimiter,
whiteList: ['127.0.0.1'],
blackList: ['13.35.67.49'],
isWhiteListed: (ip) => /^36.+$/.test(ip),
isBlackListed: (ip) => /^47.+$/.test(ip),
runActionAnyway: false,
});
// White-listed: always allowed. Black-listed: always rejected.
// If both: white-listed wins.
```
### Consume with Periodic Sync (reduce store requests)
```js
const rateLimiterMemory = new RateLimiterMemory(opts);
const rateLimiterRedis = new RateLimiterRedis({ storeClient: redisClient, ...opts });
async function consumeWithPeriodicSync(key, syncEveryNRequests = 10) {
let memoryRes = await rateLimiterMemory.consume(key);
if (memoryRes.consumedPoints % syncEveryNRequests === 0) {
const redisRes = await rateLimiterRedis.consume(key, syncEveryNRequests);
// Sync local state from Redis result
}
return memoryRes;
}
```
Sacrifices consistency for performance — useful for high-traffic endpoints.
## Error Handling Best Practices
Always distinguish between store errors and rate limit rejections:
```js
rateLimiter.consume(key)
.then((rateLimiterRes) => { /* allowed */ })
.catch((rejRes) => {
if (rejRes instanceof Error) {
// Store error (Redis down, etc.)
// Use insuranceLimiter to avoid this
} else {
// Rate limit exceeded — rejRes is RateLimiterRes
res.set('Retry-After', String(Math.round(rejRes.msBeforeNext / 1000) || 1));
res.status(429).send('Too Many Requests');
}
});
```
## Redis-Specific Setup
```js
// ioredis (default, recommended)
const Redis = require('ioredis');
const redisClient = new Redis({ enableOfflineQueue: false });
const limiter = new RateLimiterRedis({
storeClient: redisClient,
points: 5,
duration: 5,
});
// redis package v4+
const { createClient } = require('redis');
const redisClient = createClient({ /* ... */ });
await redisClient.connect();
const limiter = new RateLimiterRedis({
storeClient: redisClient,
useRedisPackage: true,
points: 5,
duration: 5,
});
```
`RateLimiterRedis` requires permissions: `+@read +@write +EVAL +EVALSHA`.
`RateLimiterRedisNonAtomic` is a non-atomic alternative — faster but subject to race conditions.
## Key Design Decisions
- **`keyPrefix` is critical** when running multiple limiters — without unique prefixes, keys collide.
- **`duration: 0`** means points never expire — useful for permanent counters.
- **`blockDuration`** extends the block beyond the normal duration window (for malicious activity).
- **`inMemoryBlockOnConsumed`** dramatically reduces store load under DoS (~7x faster per benchmarks).
- **Insurance limiter** keeps your app functional when the store is down, but data is NOT synced back when the store recovers.
- **`execEvenly`** smooths traffic peaks (Leaky Bucket style) but avoid for long durations with few points.
- Use `get()` before `consume()` for login protection — reads are cheaper than upserts, especially under DoS.
## Trust Proxy Warning
When using Express behind a reverse proxy, be careful with `trust proxy`. The `x-forwarded-for` header can be spoofed. Limit it to specific IPs or hop counts. See: https://expressjs.com/en/guide/behind-proxies.html
## Third-Party Framework Integrations
Beyond the built-in Express/Koa/Hapi middleware, community packages exist for:
- **GraphQL:** [graphql-rate-limit-directive](https://www.npmjs.com/package/graphql-rate-limit-directive)
- **NestJS:** [nestjs-rate-limiter](https://www.npmjs.com/package/nestjs-rate-limiter)
- **Fastify-based NestJS:** [nestjs-fastify-rate-limiter](https://www.npmjs.com/package/nestjs-fastify-rate-limiter)
Also supports migration from [express-brute](https://github.com/animir/node-rate-limiter-flexible/wiki/ExpressBrute-migration) and [limiter](https://github.com/animir/node-rate-limiter-flexible/wiki/RateLimiterQueue#migration-from-limiter).
## Creating Custom Limiters
Any new limiter with storage must extend `RateLimiterStoreAbstract` and implement 4 methods:
- `_getRateLimiterRes` — parse raw store data to `RateLimiterRes`.
- `_upsert` — atomic or non-atomic increment. Must support `forceExpire` mode. If non-atomic, suffix class with `NonAtomic` (e.g. `RateLimiterRedisNonAtomic`).
- `_get` — return raw data by key or `null`.
- `_delete` — delete key data, return `true`/`false`.
## Creating Custom Wrappers
For wrapper classes that don't need full `RateLimiterAbstract` functionality (like `points`, `duration`, etc.), extend `RateLimiterCompatibleAbstract` instead. This lightweight abstract class requires implementing:
- `consume`, `penalty`, `reward`, `get`, `set`, `block`, `delete` methods
- `blockDuration` and `execEvenly` getters/setters (if not used, empty no-op implementations can be provided)
Classes extending `RateLimiterCompatibleAbstract` can be used anywhere `RateLimiterAbstract` is accepted: as `insuranceLimiter`, in `RLWrapperTimeouts`, `RateLimiterUnion`, etc. See `RLWrapperBlackAndWhite` for an example implementation.