@rishabsikka/rate-limitter
v0.1.0
Published
Pluggable rate limit strategies (fixed window, token bucket, leaky bucket) with memory or Redis storage
Maintainers
Readme
@rishabsikka/rate-limitter
Pluggable rate limiting strategies for Node.js:
- Fixed window — count requests per time window
- Token bucket — burst-friendly with steady refill
- Leaky bucket — smooth outgoing / queue-style pressure
Each strategy can run on in-memory state (single process) or Redis (multiple instances / pods behind a load balancer).
Requirements
- Node.js ≥ 18
- Redis +
ioredis— only if you usedriver: "redis"(peer dependency)
Install
npm install @rishabsikka/rate-limitterIf you use Redis-backed limiters:
npm install @rishabsikka/rate-limitter ioredisQuick start (memory — single process)
const {
RateLimiterFactory,
createRateLimiterChecker,
} = require("@rishabsikka/rate-limitter");
const storage = { driver: "memory" };
const limiter = RateLimiterFactory.createLimiter(
"TOKEN_BUCKET",
"api-v1",
{ capacity: 100, refillRatePerSecond: 10 },
storage,
);
const check = createRateLimiterChecker(limiter);
// In your HTTP handler / middleware:
async function handleRequest(userId) {
const key = `user:${userId}`;
const { allowed, retryAfterMs } = await check(key);
if (!allowed) {
// return HTTP 429, set Retry-After: Math.ceil(retryAfterMs / 1000), etc.
return { status: 429, retryAfterMs };
}
return { status: 200 };
}Create one limiter instance per policy (per route / per API family) and reuse it. Do not call createLimiter on every request unless you intentionally want isolated counters.
Redis (multi-instance / production)
Use the same factory with a Redis client you construct (connection string, TLS, pool — your choice):
const Redis = require("ioredis");
const {
RateLimiterFactory,
createRateLimiterChecker,
} = require("@rishabsikka/rate-limitter");
const redis = new Redis(process.env.REDIS_URL);
const storage = {
driver: "redis",
redis,
keyPrefix: "myapp:rl", // isolates keys from other apps on the same Redis
};
const limiter = RateLimiterFactory.createLimiter(
"FIXED_WINDOW",
"login",
{ windowMs: 60_000, maxRequests: 5 },
storage,
);
const check = createRateLimiterChecker(limiter);Redis implementations use atomic Lua where needed so concurrent pods see consistent limits.
API surface
| Export | Role |
|--------|------|
| RateLimiterFactory | createLimiter(type, name, options, storage) |
| createRateLimiterChecker(limiter) | Returns async (key) => ({ allowed, retryAfterMs }) |
| RateLimiterStrategy | Abstract base class if you extend strategies |
| Memory classes | FixedWindowStrategy, TokenBucketStrategy, LeakyBucketStrategy |
| Redis classes | RedisFixedWindowStrategy, RedisTokenBucketStrategy, RedisLeakyBucketStrategy |
| Types | StorageConfig, FixedWindowOptions, TokenBucketOptions, LeakyBucketOptions, RateLimiterType |
RateLimiterType
"FIXED_WINDOW"— options:{ windowMs, maxRequests }"TOKEN_BUCKET"— options:{ capacity, refillRatePerSecond }"LEAKY_BUCKET"— options:{ capacity, leakRatePerSecond }
StorageConfig
{ driver: "memory" }{ driver: "redis", redis, keyPrefix }—redisis anioredisinstance;keyPrefixnamespaces keys (e.g.myapp:rl).
Limiter instance methods
allow(key): Promise<boolean>getRetryAfterMs(key): Promise<number>snapshot(key): Promise<object>— read-only metrics for dashboards / debugging
Choosing a key
The key string is your identity for limiting (must be stable per client you want to throttle):
- Authenticated:
user:${userId}ororg:${orgId}:user:${userId} - API key:
key:${apiKeyId} - Anonymous:
ip:${hashedIp}(be careful with NAT / mobile carriers)
Keep keys bounded in length; avoid raw huge query strings.
Framework note
This package is not tied to Express. You call createRateLimiterChecker inside your middleware (Express, Fastify, Hono, Nest, etc.) and map allowed === false to 429 + Retry-After.
Memory vs Redis
| Mode | When to use | |------|----------------| | memory | Tests, local dev, single Node process | | redis | Multiple replicas, Kubernetes, any setup where traffic hits more than one process |
Load balancers alone do not share counters; use Redis (or another shared store) for global limits.
