Advanced Usage

Table of Contents

Different Limits for Different Routes

Route-level limits let one application use different quotas for login, normal API calls, admin operations, and internal endpoints.

Express Example

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

const limiter = new RateLimiter({
  windowMs: 60 * 1000,
  max: 100,
  perRoute: {
    '/api/login': { max: 5, windowMs: 15 * 60 * 1000 },
    '/api/admin': { max: 20, windowMs: 60 * 1000 },
    '/api/public': { max: 1000, windowMs: 60 * 1000 },
  },
});

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

Egg.js Route-Level Pattern

Egg.js projects often keep route logic in middleware and use route path or route name as part of the key:

module.exports = () => {
  const limiter = new RateLimiter({
    windowMs: 60 * 1000,
    max: 100,
    keyGenerator: (ctx) => `user:${ctx.user?.id || ctx.ip}:${ctx.path}`,
  });

  return async function rateLimit(ctx, next) {
    const result = await limiter.check(`route:${ctx.path}:ip:${ctx.ip}`);
    if (!result.allowed) {
      ctx.status = 429;
      ctx.body = { error: 'Too Many Requests' };
      return;
    }

    await next();
  };
};

Custom Key Generators

Why Key Generation Matters

The key defines who shares a quota. A poor key can accidentally make unrelated users share the same limit, or let one user bypass expected limits.

Key Strategy Comparison

StrategyExampleUse CaseRisk
IPip:203.0.113.10Anonymous trafficNAT users share one quota
User IDuser:42Authenticated APIsRequires trusted identity
User + routeuser:42:/api/payBusiness locksMore keys
Tenant + actiontenant:acme:invoice:createSaaS quotasRequires tenant context
API keyapi-key:abcGateway plansKey leakage affects quota

Examples

const perIpLimiter = new RateLimiter({
  keyGenerator: (req) => `ip:${req.ip}`,
});

const perUserLimiter = new RateLimiter({
  keyGenerator: (req) => `user:${req.user?.id || 'guest'}`,
});

const perActionLimiter = new RateLimiter({
  keyGenerator: (req) => `tenant:${req.tenant.id}:user:${req.user.id}:route:${req.path}`,
});

Dynamic Configuration

Dynamic max

const limiter = new RateLimiter({
  windowMs: 60 * 1000,
  max: (req) => {
    if (req.user?.role === 'admin') return 5000;
    if (req.user?.plan === 'pro') return 1000;
    return 100;
  },
});

Dynamic skip

const limiter = new RateLimiter({
  skip: async (req) => {
    if (req.path === '/health') return true;
    return req.user?.internal === true;
  },
});

Use skip carefully. It bypasses rate limiting. For IP allowlists, prefer an independent allowlist middleware before the limiter unless bypassing quota is the explicit requirement.

IP Allowlist and Denylist

Independent Allowlist Pattern

app.get('/api/admin/users',
  ipWhitelistMiddleware('/api/admin'),
  adminLimiter.middleware(),
  handler,
);

The allowlist authorizes access. The limiter still controls request volume. See Allowlist and Rate Limit Independence.

Denylist Pattern

const blocked = new Set(['203.0.113.10']);

function denylist(req, res, next) {
  if (blocked.has(req.ip)) {
    res.status(403).json({ error: 'Forbidden' });
    return;
  }
  next();
}

Place denylist/allowlist middleware before rate limiting when you want rejected traffic to avoid consuming quota.

Redis Distributed Storage

Use Redis when multiple application instances need shared counters:

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

const redis = new Redis('redis://localhost:6379');

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

Use CacheHubStore when you want cache-hub atomic primitives:

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

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

Response Outcome Rollback

Roll Back Successful Requests

const limiter = new RateLimiter({
  skipSuccessfulRequests: true,
});

This is useful when you only want failed requests to count, such as login failures.

Roll Back Failed Requests

const limiter = new RateLimiter({
  skipFailedRequests: true,
});

This is useful when successful requests should count but failed downstream responses should not consume user quota.

Important Implementation Detail

Rollback requires internal metadata. Direct public check() results hide this metadata by default. Middleware explicitly enables it when rollback options are configured.

Custom Response Handler

const limiter = new RateLimiter({
  handler: (req, res) => {
    res.status(429).json({
      code: 429,
      message: 'Too Many Requests',
      retryAfter: Math.ceil(req.rateLimit?.retryAfter || 0),
    });
  },
});

Use a handler when your API has a standard error format.

Framework Integration Patterns

Direct check()

Use direct check() when the framework is not Express-compatible:

const result = await limiter.check(key);
if (!result.allowed) {
  throw new TooManyRequestsError(result.retryAfter);
}

Express-Compatible Middleware

Use middleware() when the framework supports (req, res, next):

app.use(limiter.middleware());

Wrapper Middleware

When a framework uses a different signature, wrap check() and map the result to the framework's own response object.

Production Notes

  • Call close() for limiter-owned Redis clients or cache-hub cleanup timers.
  • Use RedisStore or CacheHubStore with Redis when counters must be shared.
  • Keep allowlist authorization separate from rate limiting unless bypass is intentional.
  • Use business keys for sensitive operations.
  • Record benchmark environment before using benchmark results in capacity planning.
  • Keep English and Chinese docs synchronized when examples, options, or behavior change.