rate-limiter-flexible

repository·master·Indexed 25 days ago

https://github.com/animir/node-rate-limiter-flexible

A high-performance Node.js library for atomic and non-atomic counters and rate limiting to protect applications from DoS and brute-force attacks. It supports multiple storage backends including Redis, MongoDB, SQL databases (MySQL, PostgreSQL, SQLite), Memcached, DynamoDB, and in-memory storage. Version 11.2.0.

Tokens
14.9K
Snippets
42
Records
86
Agent score
84%

What's inside rate-limiter-flexible

  1. Implement Express Middleware

    master

    To use rate-limiter-flexible in an Express application, create a middleware function that calls .consume(key) on your limiter instance. If the promise resolves, call next(). If it rejects, return a 429 Too Many Requests status.

    const Redis = require('ioredis');
    const { RateLimiterRedis } = require('rate-limiter-flexible');
    
    const redisClient = new Redis({ enableOfflineQueue: false });
    const rateLimiter = new RateLimiterRedis({
      storeClient: redisClient,
      keyPrefix: 'middleware',
      points: 10,
      duration: 1,
    });
    
    const rateLimiterMiddleware = (req, res, next) => {
      rateLimiter.consume(req.ip)
        .then(() => next())
        .catch(() => res.status(429).send('Too Many Requests'));
    };
    
    app.use(rateLimiterMiddleware);
  2. Configure RateLimiterRedis with the 'redis' package

    master

    If you are using the modern redis package (v4+) instead of ioredis, you must set useRedisPackage: true and ensure you have called await redisClient.connect() before initializing the limiter.

    // redis package v4+
    const { createClient } = require('redis');
    const redisClient = createClient({ /* ... */ });
    await redisClient.connect();
    
    const limiter = new RateLimiterRedis({
      storeClient: redisClient,
      useRedisPackage: true,
      points: 5,
      duration: 5,
    });
  3. Handle Rate Limit Rejections vs Store Errors

    master

    When calling .consume(), you must distinguish between a rate limit rejection (the user is throttled) and a store error (e.g., Redis is down).

    • If the caught object is an instanceof Error, it is a store error.
    • If it is not an error, it is a RateLimiterRes object representing a rate limit rejection.
    rateLimiter.consume(key)
      .then((rateLimiterRes) => { /* allowed */ })
      .catch((rejRes) => {
        if (rejRes instanceof Error) {
          // Store error (Redis down, etc.)
          // Use insuranceLimiter to avoid this
        } else {
          // Rate limit exceeded — rejRes is RateLimiterRes
          res.set('Retry-After', String(Math.round(rejRes.msBeforeNext / 1000) || 1));
          res.status(429).send('Too Many Requests');
        }
      });
  4. Configure RateLimiterRedis with redis package v4+

    master

    If you are using the modern redis package (v4+) instead of ioredis, you must set useRedisPackage: true and call await redisClient.connect() before using the limiter.

    // redis package v4+
    const { createClient } = require('redis');
    const redisClient = createClient({ /* ... */ });
    await redisClient.connect();
    
    const limiter = new RateLimiterRedis({
      storeClient: redisClient,
      useRedisPackage: true,
      points: 5,
      duration: 5,
    });
  5. Import rate-limiter-flexible

    master

    You can import specific limiters like RateLimiterMemory using named imports, or import the limiter directly from its file path.

    import { RateLimiterMemory } from "rate-limiter-flexible";
    
    // or import directly
    import RateLimiterMemory from "rate-limiter-flexible/lib/RateLimiterMemory.js";
  6. Implement Koa Middleware

    master

    In Koa, use an async middleware that awaits rateLimiter.consume(ctx.ip). Catch rejections to set the status to 429 and provide a body, otherwise proceed with await next().

    app.use(async (ctx, next) => {
      try {
        await rateLimiter.consume(ctx.ip);
      } catch (rejRes) {
        ctx.status = 429;
        ctx.body = 'Too Many Requests';
        return;
      }
      await next();
    });
  7. Handle Errors Correctly

    master

    When using .consume(), you must distinguish between a store error (e.g., Redis is down) and a rate limit rejection (the user exceeded the limit).

    • If the caught object is an instanceof Error, it is a store error.
    • If it is not an error, it is a RateLimiterRes object representing a rate limit rejection. You should use rejRes.msBeforeNext to set a Retry-After header.
    rateLimiter.consume(key)
      .then((rateLimiterRes) => { /* allowed */ })
      .catch((rejRes) => {
        if (rejRes instanceof Error) {
          // Store error (Redis down, etc.)
          // Use insuranceLimiter to avoid this
        } else {
          // Rate limit exceeded — rejRes is RateLimiterRes
          res.set('Retry-After', String(Math.round(rejRes.msBeforeNext / 1000) || 1));
          res.status(429).send('Too Many Requests');
        }
      });
  8. Configure HTTP response headers for rate limiting

    master

    When a user is rate limited, you can use the properties of the RateLimiterRes object to populate standard HTTP headers. This informs the client when they can retry and how many requests they have left.

    const headers = {
      "Retry-After": rateLimiterRes.msBeforeNext / 1000,
      "X-RateLimit-Limit": opts.points,
      "X-RateLimit-Remaining": rateLimiterRes.remainingPoints,
      "X-RateLimit-Reset": Math.ceil((Date.now() + rateLimiterRes.msBeforeNext) / 1000)
    };
  9. Dump and restore RateLimiterMemory state

    master

    Since RateLimiterMemory is in-process, state is lost on restart. Use dump() and restore() for best-effort persistence during graceful restarts (SIGTERM/SIGINT) or blue/green deploys.

    dump() Returns a JSON-safe plain object describing every key currently held in memory:

    const snapshot = rateLimiter.dump();

    restore(data, detailResponse = false) Loads a previously dumped snapshot into the limiter.

    • If detailResponse is true, it returns an object with counts and keys per bucket (restored, expired, invalid).
    • If the snapshot is missing or has an unsupported version, it returns undefined and does not modify state.

    Caveats:

    • Key Prefix: Keys are stored without prefix; the receiving limiter applies its own keyPrefix on restore.
    • TTL: TTL is recomputed from the absolute expiresAt timestamp in the dump, not from the limiter's duration.
    • Configuration: No reconciliation of configuration changes occurs; records are restored "as-is".
    const snapshot = rateLimiter.dump();
    
    // ... later
    const result = rateLimiter.restore(snapshot);
    // or with details
    const result = rateLimiter.restore(snapshot, true);
  10. Implement an Insurance Strategy for Store Failures

    master

    To prevent your application from failing when your primary store (e.g., Redis) goes down, provide an insuranceLimiter (typically an in-memory limiter) in your configuration. If the main store fails, the insuranceLimiter is used automatically.

    Note: The insurance limiter inherits blockDuration and execEvenly from the parent, but data is not synchronized between the two stores when the main store recovers.

    const rateLimiterMemory = new RateLimiterMemory({ points: 1, duration: 1 });
    const rateLimiter = new RateLimiterRedis({
      storeClient: redisClient,
      points: 5,
      duration: 1,
      insuranceLimiter: rateLimiterMemory,
    });
    // If Redis fails, RateLimiterMemory is used automatically
  11. Implement Hapi Plugin

    master

    For Hapi, use the onPreAuth extension point. If consume fails, check if the rejection is an error (store error) or a rate limit rejection. For rate limit rejections, you can use the msBeforeNext property from the rejection object to set a Retry-After header.

    server.ext('onPreAuth', async (request, h) => {
      try {
        await rateLimiter.consume(request.info.remoteAddress);
        return h.continue;
      } catch (rej) {
        if (rej instanceof Error) {
          return Boom.internal('Try later');
        }
        const error = Boom.tooManyRequests('Rate limit exceeded');
        error.output.headers['Retry-After'] = Math.round(rej.msBeforeNext / 1000) || 1;
        throw error;
      }
    });