API Reference

Table of Contents

RateLimiter

const { RateLimiter } = require('flex-rate-limit');
const limiter = new RateLimiter(options);

check(key, options)

const result = await limiter.check('user:123', { route: '/api/data' });

middleware(options)

Creates an Express-style (req, res, next) middleware.

app.use('/api', limiter.middleware());

reset(key)

await limiter.reset('user:123');

resetAll()

await limiter.resetAll();

close()

await limiter.close();

Closes limiter-owned resources, such as Redis clients created from store: 'redis://...' and cache-hub memory cleanup timers.

RateLimiterOptions

OptionTypeDefault
windowMsnumber60000
maxnumber | function100
algorithm'sliding-window' | 'fixed-window' | 'token-bucket' | 'leaky-bucket''sliding-window'
storeStore | 'memory' | 'redis://...''memory'
keyGeneratorfunctionIP-based
headersbooleantrue
skipSuccessfulRequestsbooleanfalse
skipFailedRequestsbooleanfalse

Result Shape

{
  allowed: boolean;
  limit: number;
  current: number;
  remaining: number;
  resetTime: number;
  retryAfter: number;
}

Store Interface

Stores must provide generic counter operations and may provide algorithm-specific atomic methods.

interface Store {
  increment(key: string, options?: any): Promise<any>;
  get(key: string): Promise<any>;
  set(key: string, value: any, ttl?: number): Promise<void>;
  reset(key: string): Promise<void>;
  resetAll?(): Promise<void>;
  close?(): Promise<void>;
}

RedisStore

const { RedisStore } = require('flex-rate-limit');
const store = new RedisStore({ client: redis, prefix: 'rl:' });

External Redis clients are caller-owned by default. Set ownsClient: true when store.close() should close the client.

CacheHubStore

const { CacheHubStore } = require('flex-rate-limit');
const store = new CacheHubStore({ client: redis, prefix: 'rl:' });

CacheHubStore uses cache-hub@2.2.4 atomic state primitives. It can run with Redis or with a local in-memory cache-hub backend.

keyGenerators

const { keyGenerators } = require('flex-rate-limit');

const limiter = new RateLimiter({
  keyGenerator: keyGenerators.ip,
});
GeneratorKey shapeTypical use
keyGenerators.ipIP addressSimple public API or anonymous traffic
keyGenerators.userIduser:${id}Authenticated user limit
keyGenerators.routeAndIp${route}:${ip}Per-route anonymous limit
keyGenerators.apiEndpointapi:${route}:${ip}API endpoint-level limit
keyGenerators.userAndRouteuser:${id}:${route}Business lock / user+route limit

Detailed Method Reference

new RateLimiter(options)

Creates a limiter instance.

const limiter = new RateLimiter({
  windowMs: 60 * 1000,
  max: 100,
  algorithm: 'sliding-window',
  store: 'memory',
});

Constructor defaults:

OptionDefault
windowMs60000
max100
algorithm'sliding-window'
store'memory'
headerstrue
skipSuccessfulRequestsfalse
skipFailedRequestsfalse

check(key, options)

Checks one key directly. This is the framework-agnostic API and is the right integration point for Koa, Egg.js, Fastify, Hapi, workers, queues, and custom runtimes.

const result = await limiter.check('user:123', {
  route: '/api/orders',
  req,
});

if (!result.allowed) {
  throw Object.assign(new Error('Too many requests'), {
    retryAfter: result.retryAfter,
  });
}

Parameters:

ParameterTypeRequiredDescription
keystringyesNon-empty rate-limit key
options.reqanynoOriginal request object for dynamic max, skip, and custom context
options.routestringnoRoute context used by route-aware key generators and perRoute

Return value:

type RateLimitResult = {
  allowed: boolean;
  limit: number;
  current: number;
  remaining: number;
  resetTime: number;
  retryAfter: number;
  error?: string;
};

retryAfter is 0 when the request is allowed. When rejected, it is the suggested wait time in milliseconds.

middleware(options)

Creates an Express-style middleware function:

const limiter = new RateLimiter({
  windowMs: 60 * 1000,
  max: 100,
});

app.use('/api', limiter.middleware());

The middleware signature is:

(req: any, res: any, next?: Function) => Promise<void>

For non-Express frameworks, wrap check() instead of using the Express-style middleware directly.

reset(key)

Clears rate-limit state for a single key.

await limiter.reset('user:123');

Use this when an operator manually unlocks a user, when a test needs a clean key, or when a business workflow resets a quota.

resetAll()

Clears all state for stores that support it.

await limiter.resetAll();

MemoryStore, RedisStore, and CacheHubStore implement resetAll(). For Redis-backed stores, use a dedicated prefix so reset operations do not touch unrelated application keys.

close()

Closes limiter-owned resources.

await limiter.close();

Use close() during application shutdown, tests, benchmark scripts, and short-lived CLI tools.

Lifecycle behavior:

Store pathOwnership
store: 'memory'No external connection; memory timers are internal
store: 'redis://...'Limiter creates and owns the Redis client; close() closes it
new RedisStore({ client })External client is caller-owned by default
new RedisStore({ client, ownsClient: true })Store closes the client
new CacheHubStore()Store closes auto-created memory cleanup timers

Configuration Option Details

windowMs

The size of the rate-limit time window in milliseconds.

windowMs: 15 * 60 * 1000

Use longer windows for security-sensitive actions and shorter windows for high-throughput operational throttling.

max

Maximum allowed count for the selected algorithm. It can be a number or an async function.

max: 100

Dynamic example:

const limiter = new RateLimiter({
  max: async (req) => {
    if (req.user?.plan === 'enterprise') return 5000;
    if (req.user?.plan === 'pro') return 1000;
    return 100;
  },
});

algorithm

Supported values:

ValueUse when
sliding-windowYou need strict rolling-window fairness
fixed-windowYou need compact, approximate high-throughput limits
token-bucketYou need average quota with short bursts
leaky-bucketYou need smoother admission into a backend

store

Supported values:

ValueDescription
'memory'Default in-process store
'redis://...'RateLimiter creates and owns an ioredis client
new MemoryStore()Explicit in-memory store
new RedisStore({ client })Shared Redis-backed store
new CacheHubStore({ client })cache-hub atomic state backed by Redis
custom StoreAny object implementing the Store interface

keyGenerator

Generates the limit key from a request object.

const limiter = new RateLimiter({
  keyGenerator: (req, context) => {
    const user = req.user?.id || req.ip;
    const route = context?.route || req.route?.path || req.path || 'unknown';
    return `user-route:${user}:${route}`;
  },
});

skip

Skips rate limiting for a request.

const limiter = new RateLimiter({
  skip: (req) => {
    return req.path === '/health' || req.user?.role === 'admin';
  },
});

Common uses:

  • IP allowlist.
  • Health checks.
  • Internal service accounts.
  • Temporary operational bypasses.

handler

Customizes the response when a request is rejected.

const limiter = new RateLimiter({
  handler: (req, res) => {
    res.status(429).json({
      code: 'RATE_LIMITED',
      message: 'Too many requests',
    });
  },
});

If no handler is provided, middleware returns status 429 with a basic message.

headers

Controls whether middleware writes rate-limit headers.

headers: true

skipSuccessfulRequests and skipFailedRequests

These options are middleware-focused rollback controls.

const limiter = new RateLimiter({
  skipSuccessfulRequests: true,
});
  • skipSuccessfulRequests: count the request during processing, then roll it back if the response status is below 400.
  • skipFailedRequests: count the request during processing, then roll it back if the response status is 400 or above.

Use these options when the quota should count only failed attempts or only successful business actions. They are not needed for direct check() usage.

perRoute

Overrides options for specific routes.

const limiter = new RateLimiter({
  windowMs: 60 * 1000,
  max: 100,
  perRoute: {
    '/login': {
      windowMs: 15 * 60 * 1000,
      max: 5,
      algorithm: 'sliding-window',
    },
    '/api/upload': {
      windowMs: 60 * 1000,
      max: 20,
      algorithm: 'leaky-bucket',
    },
  },
});

Route matching uses the route context available to the limiter. When integrating manually, pass route to check().

capacity, refillRate, and leakRate

Algorithm-specific tuning options:

OptionAlgorithmDescription
capacitytoken-bucketMaximum bucket size / immediate burst allowance
refillRatetoken-bucketTokens added per windowMs
leakRateleaky-bucketWater drained per windowMs

Middleware Behavior

Middleware flow:

1. Resolve effective config, including per-route overrides.
2. Run skip(req). If true, call next().
3. Generate key.
4. Check the limit.
5. Write headers when enabled.
6. If allowed, call next().
7. If rejected, call custom handler or return 429.
8. If rollback options are enabled, observe response completion and rollback when appropriate.

Default rejection response:

HTTP 429 Too Many Requests

Use a custom handler when your API needs a structured error payload.

Headers

When headers is enabled, middleware writes common rate-limit headers:

HeaderMeaning
X-RateLimit-LimitConfigured limit
X-RateLimit-RemainingRemaining requests
X-RateLimit-ResetReset time
Retry-AfterWait time when rejected

Exact header availability can depend on the framework response object because middleware uses the Express-style response API.

Exports

CommonJS:

const {
  RateLimiter,
  MemoryStore,
  RedisStore,
  CacheHubStore,
  algorithms,
  keyGenerators,
} = require('flex-rate-limit');

ESM:

import flexRateLimit, {
  RateLimiter,
  RedisStore,
  CacheHubStore,
} from 'flex-rate-limit';

Default export:

const flexRateLimit = require('flex-rate-limit');
const limiter = new flexRateLimit.RateLimiter();

Examples

Basic Direct Usage

const { RateLimiter } = require('flex-rate-limit');

const limiter = new RateLimiter({
  windowMs: 60 * 1000,
  max: 100,
});

async function handleRequest(userId) {
  const result = await limiter.check(`user:${userId}`);
  if (!result.allowed) {
    return {
      status: 429,
      retryAfter: result.retryAfter,
    };
  }

  return { status: 200 };
}

Express Middleware

const express = require('express');
const { RateLimiter } = require('flex-rate-limit');

const app = express();
const limiter = new RateLimiter({
  windowMs: 60 * 1000,
  max: 100,
});

app.use('/api', limiter.middleware());

Koa Manual Integration

const { RateLimiter } = require('flex-rate-limit');

const limiter = new RateLimiter({
  windowMs: 60 * 1000,
  max: 100,
});

async function rateLimit(ctx, next) {
  const result = await limiter.check(`ip:${ctx.ip}`, {
    req: ctx.request,
    route: ctx.path,
  });

  if (!result.allowed) {
    ctx.status = 429;
    ctx.body = { error: 'Too many requests', retryAfter: result.retryAfter };
    return;
  }

  await next();
}

RedisStore

const Redis = require('ioredis');
const { RateLimiter, RedisStore } = require('flex-rate-limit');

const redis = new Redis(process.env.REDIS_URL);

const limiter = new RateLimiter({
  store: new RedisStore({
    client: redis,
    prefix: 'rl:',
  }),
  windowMs: 60 * 1000,
  max: 100,
});

The external Redis client remains caller-owned. Close it in your application shutdown code.

Redis Connection String

const { RateLimiter } = require('flex-rate-limit');

const limiter = new RateLimiter({
  store: 'redis://127.0.0.1:6379',
  windowMs: 60 * 1000,
  max: 100,
});

process.on('SIGTERM', async () => {
  await limiter.close();
});

In this path the limiter owns the Redis client.

CacheHubStore

const Redis = require('ioredis');
const { RateLimiter, CacheHubStore } = require('flex-rate-limit');

const redis = new Redis(process.env.REDIS_URL);

const limiter = new RateLimiter({
  store: new CacheHubStore({
    client: redis,
    prefix: 'rl:',
  }),
  algorithm: 'sliding-window',
  windowMs: 60 * 1000,
  max: 100,
});

Use CacheHubStore when you want to reuse cache-hub atomic state primitives, especially with Redis-backed state.

Business Lock Key

const limiter = new RateLimiter({
  windowMs: 60 * 1000,
  max: 10,
  keyGenerator: (req, context) => {
    const userId = req.user?.id || req.ip;
    const route = context?.route || req.path || 'unknown';
    return `business:${userId}:${route}`;
  },
});

This gives each user an independent budget for each route.

Errors and Troubleshooting

键必须是非空字符串

check() and reset() require a non-empty string key.

Fix:

await limiter.check(`user:${userId || 'anonymous'}`);

Redis client is required

new RedisStore() requires a Redis-compatible client.

Fix:

const redis = new Redis(process.env.REDIS_URL);
const store = new RedisStore({ client: redis });

resetAll() is unsupported

The store does not implement resetAll().

Fix: use a built-in store that supports it, or reset known keys individually.

Requests are all sharing one limit

The key generator is probably too coarse.

Fix: include the right identity and route dimensions:

keyGenerator: (req, context) => {
  return `user:${req.user?.id || req.ip}:${context?.route || req.path}`;
}

Redis connections remain open in tests

Call close() for limiter-owned clients, and close external clients yourself.

afterEach(async () => {
  await limiter.close();
  await redis.quit();
});