npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

redis-sliding-rate-limiter

v6.0.0

Published

Flexible and performant rate limiter based on sliding window algorithm with arbitrary precision

Downloads

2,910

Readme

redis-sliding-rate-limiter

Flexible and performant rate limiter, supporting a sliding window algorithm with arbitrary precision. Compatible with two popular Redis clients: redis, ioredis.

Table of Contents

Why another rate limiter?

Naive rate limiter algorithms suffer from a well known problem where a burst of requests at the window boundary will cause the limiter allowing more requests than what is expected for a particular key. For example, with a limit of 100 requests per minute, a client could send 100 requests in the last second of the first minute and another 100 requests in the first second of the second minute, resulting in 200 accepted requests in 2 seconds. This library supports a flexible sliding window algorithm based on Redis backend which solves this problem efficiently. The main features are the following:

  • Limiter window can be defined with arbitrary size and precision, from years down to milliseconds (e.g. a window of 3 seconds with decisecond precision or a window of 5 minutes with second precision).
  • Fast (average of 0.08 ms).
  • Each request is added to the current window with microsecond precision, avoiding losing count of concurrent requests for a particular key.
  • Redis server clock is used as the single source of truth for any time calculation.
  • For each request, expiration timestamps of the current window and the first element in that window are returned to the client. This means that the client can know precisely when a free slot will be available or when the window will be empty.
  • Express compatible middleware with customizable status code, messages and response headers.
  • Compatible with two popular Redis clients: redis, ioredis.
  • Zero dependencies.

Installation

Install package:

npm install --save redis-sliding-rate-limiter

Usage

const { createClient } = require('redis');
const { RateLimiter, Unit } = require('redis-sliding-rate-limiter');

(async () => {
  const client = createClient({
    url: `redis://localhost:6379`,
  });

  await client.connect();

  // 10 requests every 3 seconds, with decisecond precision. Allow 10% of requests to exceed the limit.
  const limiter = new RateLimiter({
    client: client,
    window: {
      unit: Unit.SECOND,
      size: 3,
      subdivisionUnit: Unit.DECISECOND, // Defines with which precision elements would expire in the current window    
    },
    limit: 10,
    limitOverhead: 0.1, // Fraction of requests that can exceed the limit, rounded down (10 * 0.1 = 1 in this example)
  });

  const key = 'OneRing';

  const results = await Promise.all([
    limiter.get(key), // 1
    limiter.get(key), // 2
    limiter.get(key), // 3
    limiter.get(key), // 4
    limiter.get(key), // 5
    limiter.get(key), // 6
    limiter.get(key), // 7
    limiter.get(key), // 8
    limiter.get(key), // 9
    limiter.get(key), // 10
    limiter.get(key), // 11 - Expect to succeed thanks to overhead
    limiter.get(key), // 12 - Expect to fail
  ]);

  for (const res of results) {
    const { allowed, remaining, firstExpireAtMs, windowExpireAtMs } = res;
    console.log(allowed, remaining, firstExpireAtMs, windowExpireAtMs);
  }

  // Output:
  //
  // true 9 1610466521278 1610466521278
  // true 8 1610466521278 1610466521278
  // true 7 1610466521278 1610466521279
  // true 6 1610466521278 1610466521279
  // true 5 1610466521278 1610466521279
  // true 4 1610466521278 1610466521279
  // true 3 1610466521278 1610466521279
  // true 2 1610466521278 1610466521279
  // true 1 1610466521278 1610466521279
  // true 0 1610466521278 1610466521279
  // true 0 1610466521278 1610466521279
  // false 0 1610466521278 1610466521279

  client.quit();
})();

Express middleware

The library exposes a configurable middleware factory that you can use directly in your Express application. See the following example:

const express = require('express');
const Redis = require('ioredis');
const { RateLimiter, Unit, createExpressMiddleware } = require('redis-sliding-rate-limiter');

(async () => {
    const app = express();

    const client = new Redis({
        host: 'localhost',
        port: 6379
    });

    const middleware = createExpressMiddleware({
        // Define limiters that will be evaluated from this middleware for each request
        limiters: [
            {
                limiter: new RateLimiter({
                    client: client,
                    window: {
                        unit: Unit.SECOND, 
                        size: 1,
                    },
                    limit: 5,
                }),
                overrideKey: true,
                // Compute Redis key from request and limiter objects. Can also be defined at middleware level (see below).
                overrideKeyFn: (req, limiter) => {
                  return req.path + limiter.name;
                },
                key: 'This key will be overridden',
                errorMessage: '[Peak] Too many requests',
            },
            {
                limiter: new RateLimiter({
                    client: client,
                    window: {
                        unit: Unit.HOUR,
                        size: 1,
                        subdivisionUnit: Unit.MINUTE,
                    },                   
                    limit: 10000, // This will be overridden                   
                }),
                overrideLimit: true,
                // Override limit if enabled. Can also be defined at middleware level (see below).
                overrideLimitFn: (req, limiter) => {
                  return parseInt(req.query.limit); // Make sure this function returns a positive integer...    
                },
                // Optional function to skip request evaluation for a specific limiter. Should return true if the evaluation must be skipped, false otherwise.
                skipFn: (req, limiter) => {
                    return false;    
                },
                errorMessage: '[Hourly] Too many requests',
            },
        ],

        // Middleware level key override. 
        // Fallback in case a limiter does not provide a overrideKeyFn function and has overrideKey enabled.
        overrideKeyFn: (req, limiter) => {
            return 'some key';
        },
      
        // Middleware level limit override.
        // Fallback in case a limiter does not provide a overrideLimitFn function and has overrideLimit enabled.
        overrideLimitFn: (req, limiter) => {
            return 666;    
        },

        // Error status code
        errorStatusCode: 429,

        // Enable/disable setting headers on response
        setHeaders: true,

        // Custom function to set headers on response object (otherwise default headers will be used)
        setHeadersFn: (req, res, limiter, limiterResponse) => {
            const {remaining, firstExpireAtMs, windowExpireAtMs} = limiterResponse;
            res.set(remainingH, '' + remaining);
            res.set(firstExpireH, '' + firstExpireAtMs);
            res.set(resetH, '' + windowExpireAtMs);
        },

        // Skip (whitelist) requests. Should return true if the request must be skipped, false otherwise
        skip: (req) => {
            return req.pleaseSkipMe;      
        },

        // Function called when a request is throttled (not allowed)
        onThrottleRequest: (req, res, key) => {
            return res.status(429).send(`Too many requests for key ${key}`);
        }
    });

    // Plug-in the middleware
    app.use(middleware);

    app.get('/', (req, res) => {
        return res.send('Yo!');
    });

    app.listen(8080, () => console.log('Server listening on port 8080...'));
})();

License

MIT License