![node version](https://img.shields.io/badge/node.js-%3E=_6.0-green.svg?style=flat-square)
node-rate-limiter-flexible
Flexible rate limiter and DDoS protector with Redis as broker allows to control requests rate in cluster or distributed environment.
It uses fixed window to limit requests.
Advantages:
- block strategy against really powerful DDoS attacks (like 100k requests per sec) Read about it and benchmarking here
- backed on native Promises
- actions can be done evenly over duration window to cut off picks
- no race conditions
- covered by tests
- no prod dependencies
- Redis errors don't result to broken app if
inMemoryLimiter
set up - useful
penalty
and reward
methods to change limits on some results of an action
Benchmark
By bombardier -c 1000 -l -d 10s -r 2500 -t 5s http://127.0.0.1:3000/pricing
Statistics Avg Stdev Max
Reqs/sec 2491.79 801.92 9497.25
Latency 8.62ms 11.69ms 177.96ms
Latency Distribution
50% 5.41ms
75% 7.65ms
90% 15.07ms
95% 27.24ms
99% 70.85ms
HTTP codes:
1xx - 0, 2xx - 25025, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Endpoint is simple Express 4.x route launched in node:latest
and redis:alpine
Docker containers by PM2 with 4 workers
Endpoint is limited by RateLimiterRedis
with config:
new RateLimiterRedis(
{
redis: redisClient,
points: 1000,
duration: 1,
},
);
Note: Performance will be much better on real servers, as for this benchmark everything was launched on one machine
Installation
npm i rate-limiter-flexible
Usage
RateLimiterRedis
Redis client must be created with offline queue switched off
const redis = require('redis');
const { RateLimiterRedis, RateLimiterMemory } = require('rate-limiter-flexible');
const redisClient = redis.createClient({ enable_offline_queue: false });
redisClient.on('error', (err) => {
});
const opts = {
redis: redisClient,
points: 5,
duration: 5,
execEvenly: false,
blockOnPointsConsumed: 10,
blockDuration: 30,
inMemoryLimiter: new RateLimiterMemory(
{
points: 1,
duration: 5,
execEvenly: false,
})
};
const rateLimiterRedis = new RateLimiterRedis(opts);
rateLimiterRedis.consume(remoteAddress)
.then(() => {
rateLimiterRedis.penalty(remoteAddress, 3);
rateLimiterRedis.reward(remoteAddress, 2);
})
.catch((rejRes) => {
if (rejRes instanceof Error) {
} else {
const secs = Math.round(rejRes.msBeforeNext / 1000) || 1;
res.set('Retry-After', String(secs));
res.status(429).send('Too Many Requests');
}
});
RateLimiterMemory
It manages limits in current process memory, so keep it in mind when use it in cluster
const rateLimiter = new RateLimiterMemory(
{
points: 1,
duration: 5,
execEvenly: false,
});
Options
-
points
Default: 4
Maximum number of points can be consumed over duration
-
duration
Default: 1
Number of seconds before points are reset
-
execEvenly
Default: false
Delay action to be executed evenly over duration
First action in duration is executed without delay.
All next allowed actions in current duration are delayed by formula msBeforeDurationEnd / (remainingPoints + 2)
It allows to cut off load peaks.
Note: it isn't recommended to use it for long duration, as it may delay action for too long
-
blockOnPointsConsumed
Default: 0
Against DDoS attacks. Blocked key isn't checked by requesting Redis.
Blocking works in current process memory.
Redis is quite fast, however, it may be significantly slowed down on dozens of thousands requests.
-
blockDuration
Default: 0
Block key for blockDuration
seconds,
if blockOnPointsConsumed
or more points are consumed
-
inMemoryLimiter
Default: undefined
RateLimiterMemory object to store limits in process memory,
when Redis comes up with any error.
Be careful when use it in cluster or in distributed app.
It may result to floating number of allowed actions.
If an action with a same key
is launched on one worker several times in sequence,
limiter will reach out of points soon.
Omit it if you want strictly use Redis and deal with errors from it
API
RateLimiterRes object
Both Promise resolve and reject returns object of RateLimiterRes
class if there is no any error.
Object attributes:
RateLimiterRes = {
msBeforeNext: 250,
remainingPoints: 0
}
rateLimiter.consume(key, points = 1)
Returns Promise, which:
- resolved when point(s) is consumed, so action can be done
- only for RateLimiterRedis: rejected when some Redis error happened, where reject reason
rejRes
is Error object - rejected when there is no points to be consumed, where reject reason
rejRes
is RateLimiterRes
object - rejected when key is blocked (if block strategy is set up), where reject reason
rejRes
is RateLimiterRes
object
Arguments:
key
is usually IP address or some unique client idpoints
number of points consumed. default: 1
rateLimiter.penalty(key, points = 1)
Fine key
by points
number of points for one duration.
Note: Depending on time penalty may go to next durations
Returns Promise
rateLimiter.reward(key, points = 1)
Reward key
by points
number of points for one duration.
Note: Depending on time reward may go to next durations
Returns Promise