mrkeyoor.com_
Sat 08 Aug 22:54 UTC
npmSecurityupdated 08 Aug 2026

rate-limiter-flexible

rate-limiter-flexible is a counter and rate-limiting toolkit for Node.js applications. A limiter gives each key, usually an IP address, account ID, token, or route-qualified identifier, a number of points for a time window and rejects consumption after those points run out. The same API works with process memory, Node clusters, Redis or Valkey, MongoDB, DynamoDB, SQL databases, Prisma, Drizzle, and Memcached. Beyond basic request throttling it supports weighted actions, temporary blocks, penalties and rewards, combined limits, FIFO queues, in-process shielding of a remote store, and an emergency insurance limiter.

Verdict

A capable choice when limits are part of application policy and must share state across processes, but it makes you own the key design, proxy trust, datastore, and outage semantics. For ordinary Express or Fastify request throttling, the framework-specific middleware is easier to configure correctly.

API stability4/5The central consume(key, points) promise contract and RateLimiterRes fields are consistent across memory and store implementations, while block, delete, penalty, reward, get, and set give each backend the same vocabulary. Eleven major versions show that the project does make breaking changes, and backend-specific flags such as useRedisPackage still expose client-version differences to callers.
Docs4/5The README explains the basic response object and links to a large wiki with separate pages for every store, option, wrapper, middleware example, queue, insurance strategy, and brute-force recipe. The strongest pages disclose operational traps such as Redis offline queues, proxy spoofing, and insurance drift. Navigation is fragmented across wiki pages, and some examples mix CommonJS with the README's newer ESM import style.
Maintenance5/5Version 11.2.0 was released on June 8, 2026 with a queue deadline feature and a PostgreSQL prepared-statement fix, and the repository was pushed the same day. GitHub reports only 9 open issues and pull requests combined. Recent major releases, bundled type declarations, current Node guidance, and coverage across several actively changing database clients show sustained maintenance.
Ecosystem5/5The package records 2,961,287 downloads for the measured week and supports Redis, Valkey, MongoDB, DynamoDB, Memcached, PostgreSQL, MySQL, SQLite, Prisma, Drizzle, Node cluster, PM2, and memory. Its wiki also documents Express, Koa, Hapi, NestJS, GraphQL, WebSocket, and AWS SDK use. Store drivers stay outside the package, which keeps the core dependency-free but leaves integration ownership with the app.

Use it if

  • You need the same consume, block, reward, and inspect API across in-memory and distributed backends
  • You need atomic counters in Redis, Valkey, MongoDB, DynamoDB, or SQL rather than a best-effort per-process middleware counter
  • You are protecting login, password-reset, API, WebSocket, job, or third-party quota operations that need different point costs and keys
  • You need to combine per-IP and per-account rules, queue callers, or keep a local emergency limiter for store failures
Skip it if

Setup reality

Install rate-limiter-flexible and use RateLimiterMemory for a single-process prototype. Current documentation marks Node 20 or newer as supported. The package has no production dependencies, so Redis, Valkey, MongoDB, PostgreSQL, Prisma, or another backing client must be installed, connected, monitored, and closed by your application. Redis needs read and write command categories plus EVAL and EVALSHA for the atomic script. With ioredis, the wiki recommends disabling the offline queue so requests do not pile up during an outage and replay later; with node-redis 4+, set useRedisPackage: true, and the wiki notes a cluster-mode problem in node-redis 4.7.0 that was fixed in v5. Give every limiter purpose its own keyPrefix or counters with different policies can collide. Decide whether a store failure should fail closed, fail open, or fall back to an insurance limiter, then test that behavior. Insurance memory is per process and its counts are not reconciled to the main store after recovery. SQL and DynamoDB limiters may create tables by default, which can require DDL permissions; set tableCreated when provisioning schema separately. Serverless SQL deployments also need explicit expired-row cleanup because the background timeout may not live long enough. Finally, normalize IPv6 and proxy-derived addresses deliberately, qualify user keys by route or action, and set Retry-After from msBeforeNext rather than hard-coding the window.

Patterns

Limit actions in one processlimit-in-memory

import { RateLimiterMemory } from 'rate-limiter-flexible';

const limiter = new RateLimiterMemory({
  points: 10,
  duration: 60,
  keyPrefix: 'api-minute',
});

await limiter.consume(userId);

Memory state is not shared across workers or servers and disappears on restart, so this is only a global limit in a single process.

Return 429 and rate-limit headers from Expressexpress-middleware

app.use(async (req, res, next) => {
  try {
    const result = await limiter.consume(req.ip);
    res.set('X-RateLimit-Remaining', String(result.remainingPoints));
    next();
  } catch (rejection) {
    if (rejection instanceof Error) return next(rejection);
    const seconds = Math.max(1, Math.ceil(rejection.msBeforeNext / 1000));
    res.set('Retry-After', String(seconds));
    res.status(429).send('Too Many Requests');
  }
});

Configure Express trust proxy to specific proxies or hop counts; trusting arbitrary X-Forwarded-For input lets callers choose the key.

Share atomic limits through ioredisshare-with-redis

import Redis from 'ioredis';
import { RateLimiterRedis } from 'rate-limiter-flexible';

const redis = new Redis(process.env.REDIS_URL, { enableOfflineQueue: false });
redis.on('error', (error) => console.error('Redis', error));

const limiter = new RateLimiterRedis({
  storeClient: redis,
  keyPrefix: 'login-ip',
  points: 20,
  duration: 60,
  blockDuration: 300,
});

Redis must permit read and write commands plus EVAL and EVALSHA. Disabling the offline queue avoids replaying a backlog after recovery.

Use the node-redis clientuse-node-redis

import { createClient } from 'redis';
import { RateLimiterRedis } from 'rate-limiter-flexible';

const redis = createClient({ url: process.env.REDIS_URL });
redis.on('error', console.error);
await redis.connect();

const limiter = new RateLimiterRedis({
  storeClient: redis,
  useRedisPackage: true,
  points: 100,
  duration: 60,
  keyPrefix: 'public-api',
});

The useRedisPackage flag is for node-redis 4+. The project wiki says node-redis 4.7.0 cluster mode is incompatible and recommends v5 where that client issue is fixed.

Charge different point costsweight-expensive-actions

const cost = operation === 'export-report' ? 10 : 1;

try {
  await limiter.consume(`${userId}:${operation}`, cost);
  await runOperation();
} catch (rejection) {
  if (rejection instanceof Error) throw rejection;
  throw new TooManyRequestsError(rejection.msBeforeNext);
}

The second consume argument is points, not seconds. A route-qualified key isolates budgets; remove the operation suffix if actions should share one pool.

Block and later clear a keytemporary-block

await limiter.block(`account:${accountId}`, 15 * 60);

// Administrative unblock or successful recovery flow
await limiter.delete(`account:${accountId}`);

A block duration of 0 means the key never expires. delete removes all counter data for the key, not only a separate block flag.

Penalize failures and reward successpenalize-and-reward

const key = `login:${accountId}`;

if (passwordIsWrong) {
  await limiter.penalty(key, 2);
} else {
  await limiter.reward(key, 1);
}

Penalty and reward adjust the current counter and can spill across durations depending on timing; delete is clearer when success should reset the whole key.

Use a local insurance limiter during Redis errorsfallback-on-store-error

import { RateLimiterMemory, RateLimiterRedis } from 'rate-limiter-flexible';

const insuranceLimiter = new RateLimiterMemory({ points: 20, duration: 60 });
const limiter = new RateLimiterRedis({
  storeClient: redis,
  keyPrefix: 'api',
  points: 20,
  duration: 60,
  rejectIfRedisNotReady: true,
  insuranceLimiter,
});

Insurance counts are per process and are never copied back to Redis, so an outage can grant more actions than the nominal distributed limit.

Stop repeatedly hitting Redis for an exhausted keyshield-remote-store

const limiter = new RateLimiterRedis({
  storeClient: redis,
  keyPrefix: 'login',
  points: 5,
  duration: 60,
  blockDuration: 300,
  inMemoryBlockOnConsumed: 5,
  inMemoryBlockDuration: 300,
});

The local block applies only to consume calls in the current process. Matching the distributed block duration keeps worker behavior less surprising.

Require both IP and account budgetscombine-ip-and-account

import { RateLimiterMemory, RateLimiterUnion } from 'rate-limiter-flexible';

const perIp = new RateLimiterMemory({ points: 100, duration: 60, keyPrefix: 'ip' });
const perAccount = new RateLimiterMemory({ points: 20, duration: 60, keyPrefix: 'account' });
const union = new RateLimiterUnion(perIp, perAccount);

await union.consume(`${req.ip}:${accountId}`);

RateLimiterUnion passes the same key to every limiter. Use separate consume calls if the IP and account components must be counted independently across different pairings.

Queue instead of rejecting outbound callsqueue-outbound-work

import { RateLimiterMemory, RateLimiterQueue } from 'rate-limiter-flexible';

const rate = new RateLimiterMemory({ points: 5, duration: 1 });
const queue = new RateLimiterQueue(rate, { maxQueueSize: 100 });

await queue.removeTokens(1);
await callThirdPartyApi();

Queueing holds callers and can exhaust memory under overload; set maxQueueSize and handle rejection rather than allowing an unbounded backlog.

Inspect and reset the current counterinspect-limit-state

const state = await limiter.get(userId);
if (state) {
  console.log({
    consumed: state.consumedPoints,
    remaining: state.remainingPoints,
    resetsInMs: state.msBeforeNext,
  });
}

await limiter.delete(userId);

get returns null for a missing or expired key. Exposing this state in an admin endpoint can reveal user identifiers, so protect and audit that endpoint.

Alternatives

PackageRegistryPick it when
express-rate-limitnpmChoose it for conventional Express request throttling with a ready-made middleware interface and standard headers
@fastify/rate-limitnpmChoose it when Fastify is the application framework and rate limiting should follow its plugin and hook model
bottlenecknpmChoose it for scheduling outbound work, concurrency caps, reservoirs, and queued jobs rather than rejecting inbound abuse
limiternpmChoose it for a smaller token-bucket or interval limiter when distributed database adapters are unnecessary