A framework-agnostic Node.js rate limiting library with multiple algorithms, Memory / Redis / cache-hub backed storage, and Express-style middleware.
- Framework-agnostic core: call
check()from Express, Koa, Egg.js, Hapi, Fastify, workers, queues, or any custom adapter. - Express-style middleware: use
limiter.middleware()directly in Express-compatible stacks. - Four algorithms: sliding window, fixed window, token bucket, and leaky bucket.
- Multiple stores: in-memory storage, Redis storage,
CacheHubStore, or your own store adapter. - Distributed-ready options: use Redis or cache-hub Redis atomic backends when counters must be shared across instances.
- Standard metadata: each check returns
allowed,limit,current,remaining,resetTime, andretryAfter. - Type definitions included: CommonJS, ESM, and TypeScript consumers are supported.
- Node.js
>=18.0.0 - npm, pnpm, or another package manager compatible with the npm registry
- Redis is optional and only needed for Redis-backed distributed rate limiting
npm install flex-rate-limitRedis-backed usage:
npm install flex-rate-limit iorediscache-hub atomic backend usage:
npm install flex-rate-limit cache-hub ioredisconst { RateLimiter } = require('flex-rate-limit');
const limiter = new RateLimiter({
windowMs: 15 * 60 * 1000,
max: 100,
});
const result = await limiter.check('user:123');
if (!result.allowed) {
console.log(`Retry after ${result.retryAfter} ms`);
}const express = require('express');
const { RateLimiter } = require('flex-rate-limit');
const app = express();
const globalLimiter = new RateLimiter({
windowMs: 15 * 60 * 1000,
max: 100,
});
app.use(globalLimiter.middleware());
const loginLimiter = new RateLimiter({
windowMs: 15 * 60 * 1000,
max: 5,
});
app.post('/api/login', loginLimiter.middleware(), (_req, res) => {
res.json({ message: 'login accepted' });
});
app.listen(3000);Use check() and map the result to the framework's own middleware, hook, or pre-handler shape:
const { RateLimiter } = require('flex-rate-limit');
const limiter = new RateLimiter({
windowMs: 60 * 1000,
max: 60,
});
async function guard(ctx, next) {
const key = `user:${ctx.user?.id || ctx.ip}:${ctx.path}`;
const result = await limiter.check(key, { route: ctx.path });
if (!result.allowed) {
ctx.status = 429;
ctx.body = { error: 'Too Many Requests', retryAfter: result.retryAfter };
return;
}
await next();
}See the runnable framework examples in examples/ and the quickstart guide in docs/getting-started/quickstart.md.
The default store is fast and simple. Use it for single-process services, local development, tests, and cases where counters do not need to be shared across instances.
const limiter = new RateLimiter({
windowMs: 60 * 1000,
max: 100,
store: 'memory',
});Use RedisStore when multiple application instances must share counters.
const Redis = require('ioredis');
const { RateLimiter, RedisStore } = require('flex-rate-limit');
const redis = new Redis(process.env.REDIS_URL || 'redis://127.0.0.1:6379');
const limiter = new RateLimiter({
windowMs: 60 * 1000,
max: 100,
store: new RedisStore({ client: redis, prefix: 'rl:' }),
});CacheHubStore keeps flex-rate-limit as the algorithm and middleware layer while delegating high-concurrency state updates to cache-hub atomic primitives. Pass a Redis client for distributed production usage; omit the client only when an in-memory cache-hub backend is acceptable.
const Redis = require('ioredis');
const { RateLimiter, CacheHubStore } = require('flex-rate-limit');
const redis = new Redis(process.env.REDIS_URL || 'redis://127.0.0.1:6379');
const limiter = new RateLimiter({
algorithm: 'sliding-window',
windowMs: 60 * 1000,
max: 100,
store: new CacheHubStore({ client: redis, prefix: 'rl:' }),
});Read more in the storage guide.
| Algorithm | Best For | Notes |
|---|---|---|
sliding-window |
Precise rolling limits | Default algorithm; stores more per-key state |
fixed-window |
High-throughput coarse windows | Fast, but requests can cluster around window boundaries |
token-bucket |
Controlled bursts | Allows bursts up to capacity and refills over time |
leaky-bucket |
Smoothing traffic | Drains at a steady rate |
Choose semantics first, then optimize storage and hot paths. See algorithm comparison and deep analysis.
const limiter = new RateLimiter({
windowMs: 60 * 1000,
max: 100,
algorithm: 'sliding-window',
headers: true,
keyGenerator: (req) => `user:${req.user?.id || req.ip}:${req.path}`,
skip: (req) => req.path === '/health',
handler: (req, res) => {
res.status(429).json({ error: 'Too Many Requests' });
},
perRoute: {
'/api/login': { max: 5, windowMs: 15 * 60 * 1000 },
'/api/users': { max: 100, windowMs: 60 * 1000 },
},
});| Option | Default | Description |
|---|---|---|
windowMs |
60000 |
Time window in milliseconds |
max |
100 |
Max requests per window; may be a function |
algorithm |
sliding-window |
One of the four supported algorithms |
store |
memory |
Store instance or 'memory' |
keyGenerator |
IP-based | Builds the rate-limit key from request context |
skip |
() => false |
Return true to bypass rate limiting |
handler |
built-in 429 response | Custom over-limit handler |
headers |
true |
Write X-RateLimit-* and Retry-After headers |
perRoute |
null |
Route-specific overrides |
skipSuccessfulRequests |
false |
Roll back successful responses |
skipFailedRequests |
false |
Roll back failed responses |
Full details are in the configuration guide and API reference.
{
allowed: true,
limit: 100,
current: 1,
remaining: 99,
resetTime: 1767225600000,
retryAfter: 0
}The repository includes reproducible local benchmark scripts for Memory direct checks, Redis direct checks, and HTTP middleware scenarios.
Run these from a cloned repository after installing development dependencies:
npm install
npm run benchmark:memory
npm run benchmark:redis
npm run benchmark:httpThe npm runtime package does not require benchmark dependencies. Redis and HTTP benchmark output records the Node.js version, Redis URL, key distribution, concurrency, and other parameters. Use BENCH_JSON=1 when you need machine-readable output.
See Benchmark and Performance for commands, environment variables, and interpretation notes.
| Entry | Description |
|---|---|
| Documentation index | Full documentation navigation |
| Quickstart | First integration path and framework examples |
| Configuration | Complete option reference and practical presets |
| Storage | Memory, Redis, and CacheHubStore selection |
| Business lock guide | User + route scoped rate limiting |
| Algorithm comparison | Choosing the right algorithm |
| API reference | Classes, options, stores, and exports |
| Benchmark guide | Local benchmark commands and caveats |
The website is built with Rspress and reuses the docs/ directory:
npm run docs:dev
npm run docs:buildRunnable examples are available in examples/:
| Category | Files |
|---|---|
| Quickstart | quickstart-express.js, quickstart-koa.js, quickstart-egg.js, quickstart-hapi.js, quickstart-fastify.js |
| Router examples | express-router-example.js, koa-router-example.js, egg-router-example.js, fastify-router-example.js |
| IP whitelist examples | express-ip-whitelist-independent.js, koa-ip-whitelist-independent.js, ip-whitelist-example.js |
| Standalone usage | standalone-example.js |
| Business lock | egg-business-lock-example.js |
npm test
npm run test:unit
npm run test:integration
npm run typecheck
npm run lint
npm run coverage- Counters are not shared across instances: use
RedisStoreorCacheHubStorewith a Redis client instead of the default Memory store. - Redis benchmarks are skipped: start Redis locally or set
REDIS_URL/BENCH_REDIS_URL. - Package installs but Redis code fails at runtime: install and configure a Redis client such as
ioredis. - Koa/Fastify/Hapi integration feels awkward: call
check()directly and wrap the result in the framework's native middleware or hook style. - Benchmark numbers differ from the docs or CI: local CPU, Node.js version, Redis latency, key distribution, and HTTP app work all affect throughput.
- monSQLize - MongoDB ORM with caching
- schema-dsl - JSON Schema validation
- jrpc - JSON-RPC 2.0 implementation