• Stars
    star
    338
  • Rank 124,931 (Top 3 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created almost 10 years ago
  • Updated almost 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Rate limiter for node.js that supports a rolling window, either in-memory or backed by redis

Rolling Rate Limiter

build status

This is an implementation of a rate limiter in node.js that allows for rate limiting with a rolling window. It can use either in-memory storage or Redis as a backend. If Redis is used, multiple rate limiters can share one instance with different namespaces, and multiple processes can share rate limiter state safely.

This means that if a user is allowed 5 actions per 60 seconds, any action will be blocked if 5 actions have already occured in the preceeding 60 seconds, without any set points at which this interval resets. This contrasts with some other rate limiter implementations, in which a user could make 5 requests at 0:59 and another 5 requests at 1:01.

Important Note: As a consequence of the way the Redis algorithm works, if an action is blocked, it is still "counted". This means that if a user is continually attempting actions more quickly than the allowed rate, all of their actions will be blocked until they pause or slow their requests.

This behavior is somewhat counterintuitive, but it's the only way that I have found that uses an atomic MULTI set of commands for Redis. Without this, race conditions would be possible. See more below..

Quick start

Basic use in an Express application.

const { RedisRateLimiter } = require("rolling-rate-limiter");

const limiter = new RedisRateLimiter({
  client: redisClient, // client instance from `redis` or `ioredis`
  namespace: "rate-limiter", // prefix for redis keys
  interval: 60000, // milliseconds
  maxInInterval: 5,
});

app.use(function (req, res, next) {
  limiter.limit(req.ipAddress).then((wasBlocked) => {
    if (wasBlocked) {
      return res.status(429).send("Too many requests");
    } else {
      return next();
    }
  });
});

Available limiters

  • InMemoryRateLimiter - Stores state in memory. Useful in testing or outside of web servers.
  • Redis rate limiters: There are two main redis clients for node: redis (aka node-redis) and ioredis. Both are supported:
    • RedisRateLimiter - Attempts to detect whether it was passed a redis or ioredis client.
    • NodeRedisRateLimiter - No detection; only works with redis client.
    • IORedisRateLimiter - No detection; only works with ioredis client.

Configuration options

  • interval: number - The length of the rate limiter's interval, in milliseconds. For example, if you want a user to be able to perform 5 actions per minute, this should be 60000.
  • maxInInterval: number - The number of actions allowed in each interval. For example, in the scenario above, this would be 5
  • minDifference?: number - Optional. The minimum time allowed between consecutive actions, in milliseconds.
  • client: Client (Redis only) - The Redis client to use.
  • namespace: string (Redis only) - A string to prepend to all keys to prevent conflicts with other code using Redis.

Instance Methods

All methods take an Id, which should be of type number | string. Commonly, this will be a user's id.

  • limit(id: Id): Promise<boolean> - Attempt to perform an action. Returns false if the action should be allowed, and true if the action should be blocked.
  • wouldLimit(id: Id): Promise<boolean> - Return what would happen if an action were attempted. Returns false if an action would not have been blocked, and true if an action would have been blocked. Does not "count" as an action.
  • limitWithInfo(id: Id): Promise<RateLimitInfo> - Attempt to perform an action. Returns whether the action should be blocked, as well as additional information about why it was blocked and how long the user must wait.
  • wouldLimitWithInfo(id: Id): Promise<RateLimitInfo> - Returns info about what would happened if an action were attempted and why. Does not "count" as an action.

RateLimitInfo contains the following properties:

  • blocked: boolean - Whether the action was blocked (or would have been blocked).
  • blockedDueToCount: boolean - Whether the action was blocked (or would have been blocked) because of the interval and maxInInterval properties.
  • blockedDueToMinDifference: boolean - Whether the action was blocked (or would have been blocked) because of the minDistance property.
  • millisecondsUntilAllowed: number - The number of milliseconds the user must wait until they can make another action. If another action would immediately be permitted, this is 0.
  • actionsRemaining: number - The number of actions a user has left within the interval. Does not account for minDifference.

Method of operation

  • Each identifier/user corresponds to a sorted set data structure. The keys and values are both equal to the (microsecond) times at which actions were attempted, allowing easy manipulation of this list.
  • When a new action comes in for a user, all elements in the set that occurred earlier than (current time - interval) are dropped from the set.
  • If the number of elements in the set is still greater than the maximum, the current action is blocked.
  • If a minimum difference has been set and the most recent previous element is too close to the current time, the current action is blocked.
  • The current action is then added to the set.
  • Note: if an action is blocked, it is still added to the set. This means that if a user is continually attempting actions more quickly than the allowed rate, all of their actions will be blocked until they pause or slow their requests.
  • If the limiter uses a redis instance, the keys are prefixed with namespace, allowing a single redis instance to support separate rate limiters.
  • All redis operations for a single rate-limit check/update are performed as an atomic transaction, allowing rate limiters running on separate processes or machines to share state safely.

Local development

Installation

Install dependencies with yarn.

To run tests, you will need to have a Redis server running. You can do this by installing Redis, and running redis-server. Alternatively, you can run the CI build, which includes tests, by installing act. This requires Docker to be running - on MacOS that means running Docker.app from your Applications folder.

Testing

  • yarn ci: Runs the CI build, including linting, type checking, and tests. Requires act to run GitHub actions locally.
  • yarn lint: Runs ESLint.
  • yarn test: Runs Jest.
  • yarn typecheck: Runs TypeScript, without emitting output.
  • yarn build: Runs TypeScript and outputs to ./lib.

More Repositories

1

pitchfinder

A compilation of pitch detection algorithms for Javascript.
TypeScript
436
star
2

solitaireVictory

A Jquery plugin that emulates the victory animation from Windows Solitaire, using your choice of DOM element(s).
JavaScript
42
star
3

inlineAB

Easy AB-testing using HTML markup and Google Analytics.
JavaScript
36
star
4

HackReactorProject

Record songs with only your voice - no instruments required!
JavaScript
32
star
5

instrumental.js

Easy-to-use sample playback for use with music apps
JavaScript
24
star
6

EarTraining

A game to help you learn ear training and become a better musician!
JavaScript
19
star
7

extended-proptypes

Useful proptypes for React
JavaScript
15
star
8

multisort

Sort an array using any number of separate, ranked criteria
JavaScript
14
star
9

functional-matrix

A matrix library for javascript supporting standard functional methods.
JavaScript
10
star
10

mandelbrotcat

The mandelbrot set in cat GIFs.
JavaScript
7
star
11

Doorbell

A doorbell for Hack Reactor.
JavaScript
6
star
12

ih

simple immutability helpers for javascript
JavaScript
5
star
13

eslint-plugin-mutation

ESLint plugin to guard against unexpected mutations of objects in javascript
JavaScript
4
star
14

restroom

One-line RESTful CRUD server in Express
JavaScript
3
star
15

redis-intro-presentation

Materials for a short talk I'm giving at Hack Reactor on the fundamentals of Redis
CoffeeScript
3
star
16

HackReactorDinnerParty

Assigns Hack Reactor students to dinner groups.
JavaScript
2
star
17

scrabblewords

Algorithm to find all words in a given hand from a given dictionary.
JavaScript
2
star
18

simpledb

An attempt to implement a basic SQL database in Rust for my learning
Rust
2
star
19

itslikebutfor

It's like X, but for Y!
JavaScript
2
star
20

MusicSpaceShooter

Arcade-style game in 3.js with a musical theme
JavaScript
2
star
21

projecteuler

Solutions to Project Euler problems I've solved, in a variety of languages.
JavaScript
2
star
22

dance-party

a dance floor with a disco theme.
JavaScript
1
star
23

TheLibraryOfBabel

Based on "The Library Of Babel", by Jorge Luis Borges
1
star
24

paras-goodbye

TypeScript
1
star
25

music-cube-client

HTML frontend for music cube project
TypeScript
1
star
26

ojala

expectaciones
CoffeeScript
1
star
27

jsheckler

Randomly changes your code to make sure your test coverage is good enough.
JavaScript
1
star