Code Monkey home page Code Monkey logo

node-ratelimiter's Introduction

ratelimiter

Rate limiter for Node.js backed by Redis.

NOTE: Promise version available at async-ratelimiter.

Build Status

Release Notes

v3.4.1 - #55 by @barwin - Remove splice operation.

v3.3.1 - #51 - Remove tidy option as it's always true.

v3.3.0 - #47 by @penghap - Add tidy option to clean old records upon saving new records. Drop support in node 4.

v3.2.0 - #44 by @xdmnl - Return accurate reset time for each limited call.

v3.1.0 - #40 by @ronjouch - Add reset milliseconds to the result object.

v3.0.2 - #33 by @promag - Use sorted set to limit with moving window.

v2.2.0 - #30 by @kp96 - Race condition when using async.times.

v2.1.3 - #22 by @coderhaoxin - Dev dependencies versions bump.

v2.1.2 - #17 by @waleedsamy - Add Travis CI support.

v2.1.1 - #13 by @kwizzn - Fixes out-of-sync TTLs after running decr().

v2.1.0 - #12 by @luin - Adding support for ioredis.

v2.0.1 - #9 by @ruimarinho - Update redis commands to use array notation.

v2.0.0 - API CHANGE - Change remaining to include current call instead of decreasing it. Decreasing caused an off-by-one problem and caller could not distinguish between last legit call and a rejected call.

Requirements

  • Redis 2.6.12+
  • Node 8.0.0+

Installation

$ npm install ratelimiter

Example

Example Connect middleware implementation limiting against a user._id:

var id = req.user._id;
var limit = new Limiter({ id: id, db: db });
limit.get(function(err, limit){
  if (err) return next(err);

  res.set('X-RateLimit-Limit', limit.total);
  res.set('X-RateLimit-Remaining', limit.remaining - 1);
  res.set('X-RateLimit-Reset', limit.reset);

  // all good
  debug('remaining %s/%s %s', limit.remaining - 1, limit.total, id);
  if (limit.remaining) return next();

  // not good
  var delta = (limit.reset * 1000) - Date.now() | 0;
  var after = limit.reset - (Date.now() / 1000) | 0;
  res.set('Retry-After', after);
  res.send(429, 'Rate limit exceeded, retry in ' + ms(delta, { long: true }));
});

Result Object

  • total - max value
  • remaining - number of calls left in current duration without decreasing current get
  • reset - time since epoch in seconds at which the rate limiting period will end (or already ended)
  • resetMs - time since epoch in milliseconds at which the rate limiting period will end (or already ended)

Options

  • id - the identifier to limit against (typically a user id)
  • db - redis connection instance
  • max - max requests within duration [2500]
  • duration - of limit in milliseconds [3600000]

License

MIT

node-ratelimiter's People

Contributors

barwin avatar fgribreau avatar kadishmal avatar kikobeats avatar knoxcard2 avatar kp96 avatar kwizzn avatar luin avatar noamshemesh avatar peng-huang-ch avatar promag avatar ronjouch avatar svyandun avatar tj avatar xdmnl avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

node-ratelimiter's Issues

Possible units confusion

Is the reset parameter:

  • supposed to be expressed in seconds or milliseconds?
  • supposed to express a time interval or an absolute time?

README.md says:

reset - time in milliseconds until the end of current duration

#36 says:

The reset header value is the time since epoch in seconds that the 
rate limiting period will be ended (or already ended).

My understanding from trial and error and reading the code is that it's an absolute time and is expressed in seconds.

Rate limit max value

Case:
We need to cover rate limiting with tests, but we could not reproduce case when we reach out the limit, we are trying to default value to 0, but it's automatically defaulted to 2500. Is it possible to resolve such case?

opts tidy is a bug?

var tidy = this.key;

function Limiter(opts) {
  this.id = opts.id;
  this.db = opts.db;
  this.tidy = opts.tidy || false;
  // ... ...
}

emmm,but tidy = this.key !

Limiter.prototype.get = function (fn) {
  var db = this.db;
  var tidy = this.key;
  var duration = this.duration;
  // ... ...
}

Reatelimiter not able to create redis key

function create() {
var ex = (Date.now() + duration) / 1000 | 0;
//written for testing code
db.set([count, max, 'PX', duration, 'NX'],function(er,re){
console.log("single set",er,re);
//error message "(error) ERR wrong number of arguments for 'set' command"
})

db.multi()
  .set([count, max, 'PX', duration, 'NX'])
  .set([limit, max, 'PX', duration, 'NX'])
  .set([reset, ex, 'PX', duration, 'NX'])
  .exec(function (err, res) {
    if (err) return fn(err);
    // If the request has failed, it means the values already
    // exist in which case we need to get the latest values.
    if (isFirstReplyNull(res)) return mget();//Going to infinite loop here

    fn(null, {
      total: max,
      remaining: max,
      reset: ex
    });
  });

}

adapters

not just redis, but meh for now

How To use? Update Readme

Hi. This package is hard to understand how to use.

The project https://github.com/jhurliman/node-rate-limiter is similar in nature but yet the README there is enough to enable me to get started. The downside is that I need a redis datastore and it does not support that. I come to this project and it is not clear to me on how to use this.

I might submit a PR if I'm able to figure it out. Thanks!

Reset functionality

We are trying to use node-ratelimiter for BFD protection purposes of logins.
This is easy to do, the only issue is that there is no way to reset after a successful login.

Good idea to add this to the code? Any idea on where to start?

Getting a negative number for expiration

I'm getting a negative number for expiration causing my redis client to come back with this error:

[ [Error: ERR invalid expire time in SETEX] ]

Line 105 uses this:

ex * 1000 - Date.now()

where my ex is 1441268386 and Date.now() was 1441268386507 meaning it was -507ms over expiration.

The problem here is that error is in a weird double array so the error check doesn't catch it and my responses to limit.get(function(err, limit) { ... }); always show n remaining so it effectively doesn't rate limit at all.

I assume other people would have run into this. I was using a duration of 1 second and I'm hitting redis pretty hard concurrently.

Is this line stripping off the milliseconds and just making it accurate to the second?

var ex = (Date.now() + duration) / 1000 | 0;

limit.reset always 1499329365

In your example the value of "limit.reset"
res.set('X-RateLimit-Reset', limit.reset);
is always "1499329365". Everything else works fine but this value is unchanged.
And the "duration" option will not be accepted.
var limit = new Limiter({ id: apikey, db: sails.redis, max: 100, duration : 3600000 });

Any `get` call will further delay a positive `remaining` value

Hello!

This line says to me that by calling the Limiter.prototype.get method, an entry will always be added during the computation of the the count, and more importantly, the returned remaining values.

In laymen's terms, it seems like the caller is "charged" a count value, even when ultimately the limiter willmay tell them that they have zero remaining. This can result in the goalpost continuing to move for an impolite/naive caller. This may be what we want, but it may not be. A poorly written caller script may end up perpetually waiting.

It would be nice if there were a way to only add the element to the array if remaining > 0.

I'm not a Redis expert, but I realize that the commands are issued within a MULTI transaction, and recognize the benefits there that it may not be a good idea to only perform the ZADD if we think there are "tokens" remaining (due to concurrency realities). Perhaps:

  • Some crazy if/else can be done in the Redis commands that only calls ZADD if the ZCARD length was below the max?
  • Maybe some of the ZADD options can support something useful with this idea?
  • Perhaps if the result is that the remaining <= 0, then element added by ZADD is removed in a subsequent Redis command? We know the element we created, right? This may open the user up to slightly race-vulnerable conditions (or not) that they can decide if they are OK with?

Seems semi-related to:
#43
and
#45

Memory consumption

Hi, thanks for the lib.

I'm trying to size my redis instance. Do we have any idea of the memory consumption of this lib?

For example, let's say I want to prevent more than 10 access/min and I have 2000 different client IPs per day?

What would that give?

Race condition when using `async.times`

When I tested the limiter with the code below

var redis = require('ioredis'),
  Limiter = require('ratelimiter'),
  async = require('async'),
  db = redis.createClient(),
  limiter;

limiter = new Limiter({
  id: 'hello',
  db: db
});

async.times(10, function(n, next) {
  limiter.get(next);
}, function(err, res) {
  console.log(res);
});

It printed the following

[ { total: 2500, remaining: 2500, reset: 1485859856 },
  { total: 2500, remaining: 2499, reset: 1485859856 },
  { total: 2500, remaining: 2499, reset: 1485859856 },
  { total: 2500, remaining: 2499, reset: 1485859856 },
  { total: 2500, remaining: 2499, reset: 1485859856 },
  { total: 2500, remaining: 2499, reset: 1485859856 },
  { total: 2500, remaining: 2499, reset: 1485859856 },
  { total: 2500, remaining: 2499, reset: 1485859856 },
  { total: 2500, remaining: 2498, reset: 1485859856 },
  { total: 2500, remaining: 2498, reset: 1485859856 } ]

Probably due to n value not getting updated. Sent a pr to try fixing this issue. Comments are appreciated.

one-off error with `limit.remaining`

The readme shows using limit.remaining as the recommended check on whether to rate-limit or not. This leads to unexpected behavior where there is a one-off error, here is the most simple example of max set to 2:

var id = req.user._id;
var limit = new Limiter({ id: id, db: db, max: 2 });
limit.get(function(err, limit) {
  if (err) return next(err);

  // all good
  debug('remaining %s/%s %s', limit.remaining, limit.total, id);
  if (limit.remaining) return next();

  // not good - rate-limit
  res.send(429);
});

In this example, the first time we call limit.get() remaining will get set to 1 and the second time we call it remaining will get set to 0, which will have us rate-limit the request, effectively only allowing a max of 1, instead of our intended 2.

One thought I had on how to fix this, is to allow the lower bound value of limit.remaining to be -1 instead of 0.

then instead of doing if (limit.remaining) next() we could do if (limit.remaining >= 0) next()

min-interval option

Thank you for the awesome module.

I want the ability to block and not after a few seconds from the last access.
(In order to prevent the continuous access of a very short period of time)

Sorry my English

Race condition...

Is there are reason this doesn't use the built-in redis decr instead of fetch value n then set value n - 1? What if n changes after the get but before the save?

limiter never frees up

const RateLimiter = require('ratelimiter')
//const RateLimiter = require('.')
const Redis = require('ioredis')
const { promisify } = require('util')

const limiter = new RateLimiter({
  duration: 2000,
  max: 2,
  id: 'something',
  db: new Redis()
})

const get = promisify(limiter.get.bind(limiter))

let index = -1
setInterval(async () => {
  console.log('---')
  console.log('iteration', ++index)
  const r = await get()
  console.log('value', r)
}, 100)

r.remaining goes to 0 and stays at 0. it's expected to release at least 1 every 2 seconds.

initial issue on sub-project: microlinkhq/async-ratelimiter#11

Misleading reset value

Since a new member is added to the sorted set every time the rate limiter is accessed, the sorted set will keep growing even if the limit is reached.
It leads to situation where, clients that are waiting for the limit to be reached before retrying when specified can end up in a loop of requests that will be limited.

Simplified example:

limit = 2
duration = 3
time sorted set result
t0.0 [0.0] {remaining: 2, reset: 3.0}
t0.1 [0.0, 0.1] {remaining: 1, reset: 3.0}
t0.2 [0.0, 0.1, 0.2] {remaining: 0, reset: 3.0}
t3.0 [0.1, 0.2, 3.0] {remaining: 0, reset: 3.1}
t3.1 [0.2, 3.0, 3.1] {remaining: 0, reset: 3.2}

Shouldn't the rate limiter remove the newly added member if remaining is <= 0?
With my simplified example we would have something like:

time sorted set result
t0.0 [0.0] {remaining: 2, reset: 3.0}
t0.1 [0.0, 0.1] {remaining: 1, reset: 3.0}
t0.2 [0.0, 0.1] {remaining: 0, reset: 3.0}
t3.0 [0.1, 3.0] {remaining: 1, reset: 3.1}
t3.1 [3.0, 3.1] {remaining: 0, reset: 6.0}

TTL instead of multiple keys.

Not sure if the understand the module completely, so apologies if I am wrong. I was wondering if maybe the module could set a single key and use TTL to find out 'X-RateLimit-Reset'.

Compatibility issue with ioredis-mock

Currently ioredis-mock only supports multi() when the transactional commands are passed as an array of arguments. How easily could you switch to the supported array syntax?

ex.
Change all multi's from

db.multi().set('foo', 'bar').get('foo')...
to
db.multi([ ['set', 'foo', 'bar'], ['get', 'foo'] ])...

Also, I verified that this array syntax for multi is also compatible with ioredis, node_redis, and fakeredis.

add zremrangebyrank

add zremrangebyrank after here to remove some useless data information.

db.multi()
    .zremrangebyscore([key, 0, start])
    .zcard([key])
    .zadd([key, now, now])
    .zrange([key, 0, 0])
    .zrange([key, -max, -max])
    //.zremrangebyrank([key, 0, -(max + 1)])
    .pexpire([key, duration])
    .exec(function (err, res)

reset limiter manually

sometimes you need to reset limiter, so what about calling limiter.reset to get all Redis keys reinitialized as if this limiter is just created now

how to use ?

   var Limiter = require("ratelimiter");
    var redis = require("redis");
    var redis_client = redis.createClient();
    var limit = new Limiter({
        db: redis_client,
        duration: 60000,
        max: 10,
        id: function (req) {
            console.log(req.ip);
            return req.ip;
        }
    });
    app.post("/kk",  limit.get(function(err, limit){
        if (err) return next(err);

        res.set('X-RateLimit-Limit', limit.total);
        res.set('X-RateLimit-Remaining', limit.remaining - 1);
        res.set('X-RateLimit-Reset', limit.reset);

        // all good
        debug('remaining %s/%s %s', limit.remaining - 1, limit.total, id);
        if (limit.remaining) return next();

        // not good
        var delta = (limit.reset * 1000) - Date.now() | 0;
        var after = limit.reset - (Date.now() / 1000) | 0;
        res.set('Retry-After', after);
        res.send(429, 'Rate limit exceeded, retry in ' + ms(delta, { long: true }));
    }), done_kk);

----can not use like the above?

In concurrent environment, the race condition occurs

Imagine when there are two or more clients which handle the users' requests. There can be multiple requests (eg. 2) from the same user at the same time.

Imagine the current count is 10, both requests read this value and try to set the updated +1 = 11. They both succeed, but the count is set to 11 instead of 12.

The solution we can use is WATCH Redis command as explained in the Optimistic locking using check-and-set subsection of Transactions.

I'm going to send a PR for this as it's extremely critical for our environment.

Stuck with a previous max limit

Hey guys,

I've started using your module, but I realized that if you change the max limit (after some requests), the module stays limited with the first one.

Thanks,
Philmod

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.