Coder Social home page Coder Social logo

jhurliman / node-rate-limiter Goto Github PK

View Code? Open in Web Editor NEW
1.5K 16.0 131.0 218 KB

A generic rate limiter for node.js. Useful for API clients, web crawling, or other tasks that need to be throttled

License: MIT License

JavaScript 4.27% TypeScript 95.73%

node-rate-limiter's Introduction

limiter

Build Status NPM Downloads

Provides a generic rate limiter for the web and node.js. Useful for API clients, web crawling, or other tasks that need to be throttled. Two classes are exposed, RateLimiter and TokenBucket. TokenBucket provides a lower level interface to rate limiting with a configurable burst rate and drip rate. RateLimiter sits on top of the token bucket and adds a restriction on the maximum number of tokens that can be removed each interval to comply with common API restrictions such as "150 requests per hour maximum".

Installation

yarn add limiter

Usage

A simple example allowing 150 requests per hour:

import { RateLimiter } from "limiter";

// Allow 150 requests per hour (the Twitter search limit). Also understands
// 'second', 'minute', 'day', or a number of milliseconds
const limiter = new RateLimiter({ tokensPerInterval: 150, interval: "hour" });

async function sendRequest() {
  // This call will throw if we request more than the maximum number of requests
  // that were set in the constructor
  // remainingRequests tells us how many additional requests could be sent
  // right this moment
  const remainingRequests = await limiter.removeTokens(1);
  callMyRequestSendingFunction(...);
}

Another example allowing one message to be sent every 250ms:

import { RateLimiter } from "limiter";

const limiter = new RateLimiter({ tokensPerInterval: 1, interval: 250 });

async function sendMessage() {
  const remainingMessages = await limiter.removeTokens(1);
  callMyMessageSendingFunction(...);
}

The default behaviour is to wait for the duration of the rate limiting that's currently in effect before the promise is resolved, but if you pass in "fireImmediately": true, the promise will be resolved immediately with remainingRequests set to -1:

import { RateLimiter } from "limiter";

const limiter = new RateLimiter({
  tokensPerInterval: 150,
  interval: "hour",
  fireImmediately: true
});

async function requestHandler(request, response) {
  // Immediately send 429 header to client when rate limiting is in effect
  const remainingRequests = await limiter.removeTokens(1);
  if (remainingRequests < 0) {
    response.writeHead(429, {'Content-Type': 'text/plain;charset=UTF-8'});
    response.end('429 Too Many Requests - your IP is being rate limited');
  } else {
    callMyMessageSendingFunction(...);
  }
}

A synchronous method, tryRemoveTokens(), is available in both RateLimiter and TokenBucket. This will return immediately with a boolean value indicating if the token removal was successful.

import { RateLimiter } from "limiter";

const limiter = new RateLimiter({ tokensPerInterval: 10, interval: "second" });

if (limiter.tryRemoveTokens(5))
  console.log('Tokens removed');
else
  console.log('No tokens removed');

To get the number of remaining tokens outside the removeTokens promise, simply use the getTokensRemaining method.

import { RateLimiter } from "limiter";

const limiter = new RateLimiter({ tokensPerInterval: 1, interval: 250 });

// Prints 1 since we did not remove a token and our number of tokens per
// interval is 1
console.log(limiter.getTokensRemaining());

Using the token bucket directly to throttle at the byte level:

import { TokenBucket } from "limiter";

const BURST_RATE = 1024 * 1024 * 150; // 150KB/sec burst rate
const FILL_RATE = 1024 * 1024 * 50; // 50KB/sec sustained rate

// We could also pass a parent token bucket in to create a hierarchical token
// bucket
// bucketSize, tokensPerInterval, interval
const bucket = new TokenBucket({
  bucketSize: BURST_RATE,
  tokensPerInterval: FILL_RATE,
  interval: "second"
});

async function handleData(myData) {
  await bucket.removeTokens(myData.byteLength);
  sendMyData(myData);
}

Additional Notes

Both the token bucket and rate limiter should be used with a message queue or some way of preventing multiple simultaneous calls to removeTokens(). Otherwise, earlier messages may get held up for long periods of time if more recent messages are continually draining the token bucket. This can lead to out of order messages or the appearance of "lost" messages under heavy load.

License

MIT License

node-rate-limiter's People

Contributors

c-saunders avatar callmewa avatar chrisvxd avatar cwolters avatar denisgorbachev avatar fela98 avatar jhurliman avatar luto avatar mbolis avatar nicolashenry avatar rickhuizinga avatar stiang avatar syko avatar zeke 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  avatar  avatar  avatar

node-rate-limiter's Issues

How to save / load the limiter objects' status?

Hi All,
Imagine I wanted to write a command line script in Node that needs to be rate-limited across multiple executions, e.g. a Twitter API client. I call it once, the limiter takes note of it, the script terminates. Then I call it again, the limiter is aware of the previous call, checks the limiting etc.

How do I manage this need with this library? E.g. can I "serialize" in some way the status of the library's objects, so that I can save them and re-load them back every time I run the script? I wouldn't want to use any backend component, as in the choice of Redis for classdojo/rolling-rate-limiter, but would be happy with something simpler and slower, such as file system or environment variables.

Thanks.

Add tokens or dynamically set number of tokens

I am using limiter to throttle calls to a DynamoDB table. DynamoDB will return the Consumed Capacity, which sometimes is 0. I would like to NOT delay the next call if the Consumed Capacity is 0 but I don't see a straight forward way to do this. I have tried setting then number of tokens to 2, then running tryRemoveTokens(1) if Consumed Capacity is > 0. I also tried the opposite and this.limiter.tokensThisInterval+=1; if Consumed Capacity is 0. Neither seems to work the way I thought it would. tryRemoveTokens would run into if (count > this.tokenBucket.tokensPerInterval - this.tokensThisInterval) and return false.

I am probably misunderstanding how this works. Here is example code snippets.

limiter = new RateLimiter(2, 1000);
limiter.removeTokens(2, (err, remainingRequests) => {
   query.returnConsumedCapacity().exec((err, items) => {
        if (items.ConsumedCapacity > 0 || items.Count > 0) {
           limiter.tryRemoveTokens(1);
         }
    });
});
limiter = new RateLimiter(1, 1000);
limiter.removeTokens(1, (err, remainingRequests) => {
   query.returnConsumedCapacity().exec((err, items) => {
        if (items.ConsumedCapacity === 0 || items.Count === 0) {
            this.limiter.tokensThisInterval+=1;
         }
    });
});

This script is still vulnerable to while loop spams ~

i=0;while (i < 125){ cb.socket.send('JOIN b4b3872292a3cf3905c14d4722ad25193f00f41c 2 {NS US-01}'); i++; }

Something as simple as this goes so fast, it bypasses the rate limiter even when set at no more than 2 requests per second.

I recommend using Date.now() and checking it against their last session request and closing the connection, and using this limiter script as well. Use both, but don't rely just on this script for protection.

Allow sync method

It would be great if callback could be ignored if it's undefined. Right now it throws an error.

For example, I want to removeTokens but I don't care about the callback. Perhaps you could add a removeTokensSync method? and in removeTokens just check if callback is not null and is a function.

Possible to adjust concurrency?

Hi. I'm hitting the GitHub API using this setting:

const limiter = new RateLimiter(5000, 'hour')

This is within GitHub's documented limit, but I'm still getting throttled because limiter is making so many concurrent requests. I think this is happening because limiter comes on strong at first and uses some kind of backoff mechanism to taper down the requests over the course of the hour. Is that right?

Is there a way to limit the number of concurrent requests?

Here's one way I'm thinking I could approximate it:

const limiter = new RateLimiter(Math.floor(5000/60), 'minute')
// or
const limiter = new RateLimiter(Math.floor(5000/60/60), 'second')

Able to rate limit by parameter?

I'm going to keep digging into the code to see what's possible, but in the event that someone can provide a quick answer and save me some time, it seems worth asking the question. I have an API backed by ExpressJS. For a handful of endpoints, I need to ensure that those endpoints can't be accessed more than n times per hour per identifier.

In other words, it's not that the endpoint itself can't be hit more than 3 times an hour, but rather that it can't be hit more the 3 times an hour by any given [insert parameter]. In every case, the parameter is part of the request (e.g. req.body.identifier).

Anything jump out to say that this can't be done?

Monotonic Time

If the computer's time changes, Date() will be affected, that's why a monotonic time should be used, such as process.hrtime.

Setting up the rate limit

Hi,

Great job with the package I just have some questions?

var RateLimiter = Npm.require('limiter').RateLimiter;
        var cache = Npm.require('memory-cache');
        checkBy = req.headers.app_id;
        if (cache.get(checkBy)){
            var cachedLimiter = cache.get(checkBy); 
            if (cachedLimiter.getTokensRemaining()>1){
                cachedLimiter.removeTokens(1, function(){});
                cache.put(checkBy, cachedLimiter, 60000);
                return;
            }
            throw new Error('Too many requests for the app_id: ' + checkBy + '!');
        }
        else {
            var cachedLimiter = new RateLimiter(10, 'sec'); 
            cache.put(checkBy, cachedLimiter, 60000); 
            return;
        }
  1. If I put var cachedLimiter = new RateLimiter(10, 'sec'); the rate limit is now working as expected I put 30 calls in the same second and about 25 gets passed and 5 gets rejected because of the limit although I don't want to allow more than 10 per second? Could you tell me what am I doing wrong?
  2. Can I activate two limits at once, one for 10 requests per second with 60 sec stop (this one I have done in the code above) and one for 400 requests per hour with an hour stop if they reach the second limit? Do I have to do this with a parentToken? If yes I couldn't find anywhere any description on how to use the parentToken?

Thank you very much in advance.

Best regards!

Limiter does not work

My code is as following:

constructor:()=>{
  this.RateLimiterOnSeconds = new RateLimiter(4, 1*1000); // 5 requests per second
  this.RateLimiterOnHours = new RateLimiter(2000, 1*1000*60*60); // 2000 request per hour
}
sendReq1: ()=>{
  schedule(..)
}
sendReq2: ()=>{
  schedule(...)
}
schedule: (fn)=>{
  this.RateLimiterOnSeconds.removeTokens(1,(err, remainingRequests)=>{
    this.RateLimiterOnHours.removeTokens(1, () => {
      console.log('request executed in second...',new Date().getTime()/1000, remainingRequests);
      fn();
    });
  });
}

The limit is exceeded.

Indeed, the console.log in the schedule() method prints:

request executed in second... 1487770295.951 4
request executed in second... 1487770296.043 4
request executed in second... 1487770296.219 4
request executed in second... 1487770296.258 4
request executed in second... 1487770296.427 4
request executed in second... 1487770296.56 4
request executed in second... 1487770296.628 4
request executed in second... 1487770296.803 4
request executed in second... 1487770296.831 4
request executed in second... 1487770296.921 4
request executed in second... 1487770297.126 4
request executed in second... 1487770297.214 4
request executed in second... 1487770297.311 4
request executed in second... 1487770297.421 4
request executed in second... 1487770297.605 4
request executed in second... 1487770297.686 4
request executed in second... 1487770297.803 4

and as you can see, in the second 1487770297 there are 7 requests (instead of 5), and number of tokens to remove is constant.

Tiered Rate Limiting - Extending TokenBucket to support such functionality.

I am calling an API that makes the specific requirements.

  1. You can only call said API 60 times / minute.
  2. You can only call said API 1000 times / day.

Obviously, you can satisfy the #1 by using new RateLimiter({"tokensPerInterval":60 ,"interval":"minute"}); You could then create a TokenBuffer for #2 with a parent as #1. However, the TokenBuffer does not add tokens up front and also 'drips' tokens in as time goes by.

Hence, I looked at another solution of using 2 tiered TokenBuckets with behavior similar to RateLimiter.

In order to make a TokenBucket behave initially like the RateLimiter, during construction, it is necessary to see the content at construction time.

This is currently done in RateLimiter as follows:
this.tokenBucket.content = tokensPerInterval;

However, as this is a TokenBucket and not a RateLimiter, tokens are only added during calls to drip(). Ideally, to make TokenBucket behave in the same fashion as RateLimiter, a change to drip() was required.

Here's the code excerpt I modified such that you pass a value for milliseconds (necessary to allow extension of drip() for custom behavior by subclasses) and then use milliseconds accordingly.

this.drip(clock_1.getMilliseconds());

/**
  * Add any new tokens to the bucket since the last drip.
  * @returns {Boolean} True if new tokens were added, otherwise false.
  */
 drip(milliseconds) {
     if (this.tokensPerInterval === 0) {
         const prevContent = this.content;
         this.content = this.bucketSize;
         return this.content > prevContent;
     }
     
     const now = milliseconds;
     const deltaMS = Math.max(now - this.lastDrip, 0);
     this.lastDrip = now;
     const dripAmount = deltaMS * (this.tokensPerInterval / this.interval);
     const prevContent = this.content;
     this.content = Math.min(this.content + dripAmount, this.bucketSize);
     return Math.floor(this.content) > Math.floor(prevContent);
 }

Next, you need to create the functionality you need with a custom drip() and initial seed of the content. Here's a class that does just that.

class RateLimiterEx extends TokenBucket {
    constructor({ bucketSize, tokensPerInterval, interval, parentBucket}) {
        super({bucketSize,  tokensPerInterval, interval, parentBucket});
        // initially fill the bucket.
        this.content = this.bucketSize;
    }

    // Overload drip() such that we add tokens only when the interval expires.
    drip(milliseconds) {
        if (this.tokensPerInterval === 0) {
            const prevContent = this.content;
            this.content = this.bucketSize;
            return this.content > prevContent;
        }
        // When timer expires, we simply reset content to the bucketSize.
        const now = milliseconds;
        const deltaMS = Math.max(now - this.lastDrip, 0);
        const prevContent = this.content;
        if (deltaMS >= this.interval) {
            this.content = this.bucketSize;
            this.lastDrip = now;
        }
        return Math.floor(this.content) > Math.floor(prevContent);
    }
}

With this implementation, fireImmediately is not supported. Also, there is a slight difference in the 'sleep' behavior such that TokenBucket will sleep 1000ms while awaiting a refresh (to ensure drip() is called every so often whereas RateLimiter sleeps for a larger amount of time.

Here's some sample code that I put together that illustrates the usage.

const { RateLimiter,TokenBucket } = require("limiter");

async function mainTB4() {
    optionsTier1 = {"tokensPerInterval":60, "interval":"minute"};
    optionsTier2 = {"tokensPerInterval":80, "interval":1000*80};
    const tb = new RateLimiterEx({...optionsTier1});
    const tb2 = new RateLimiterEx({...optionsTier2,"parentBucket":tb});
    let start = new Date().valueOf();
    let delta = start;
    for (let i=0;i<330;i++) {
        //await sleep(100);
        let now = new Date().valueOf();
        console.log("i:%d el:%d d:%d cr:%o", i,now - start, now - delta, await tb2.removeTokens(1)); 
        delta = now;
    }
}

mainTB4();

Attached please find sample code and changes to TokenBucket.js.
RateLimiterEx.zip

I would welcome comments on this. IMO, this approach supports multiple levels of tiered limiting.

  1. You can only call said API 60 times / minute.
  2. You can only call said API 1000 times / month.
  3. You can only call said API 5000 times / year.

Would this work with await?

Hi,

I have something like this:

import {RateLimiter} from 'limiter';
const dynamoDBLimiter = new RateLimiter(10, 'second');
console.log('before removeTokens');
for(let contact_i = 0; contact_i < company.contacts.length; contact_i++) {
  dynamoDBLimiter.removeTokens(1, async function(err, remainingRequests) {
	console.log('adding contact');
	newContact = await addContact(contact_i);
	console.log('new contact added');
  });
}
console.log('end of removeTokens');

But the logs I get are like this:

before removeTokens
end of removeTokens
adding contact
new contact added

Am I doing something wrong?
Thanks!

Callbacks processed out of order?

It looks like maybe the callbacks provided to removeTokens can be called out of order. I'm not sure if ordering is an intended feature, but if not it'd be good call that out clearly in the docs.

var RateLimiter = require('limiter').RateLimiter;
var l = new RateLimiter(1, 10000);
var i = 0;
var f = () => {
  var j = i++;
  var d = new Date();
  l.removeTokens(1, () => console.log(`${j}: enqueued: ${d}; dequeued: ${new Date()}`));
}
for (var k = 0; k < 5; k++) setTimeout(() => f(), 1000*k);

Running this code several times shows arbitrary ordering:

> i = 0
> for (var k = 0; k < 5; k++) setTimeout(() => f(), 1000*k);
0: enqueued: Thu Feb 01 2018 12:41:18 GMT-0800 (PST); dequeued: Thu Feb 01 2018 12:41:18 GMT-0800 (PST)
2: enqueued: Thu Feb 01 2018 12:41:20 GMT-0800 (PST); dequeued: Thu Feb 01 2018 12:41:28 GMT-0800 (PST)
1: enqueued: Thu Feb 01 2018 12:41:19 GMT-0800 (PST); dequeued: Thu Feb 01 2018 12:41:38 GMT-0800 (PST)
3: enqueued: Thu Feb 01 2018 12:41:21 GMT-0800 (PST); dequeued: Thu Feb 01 2018 12:41:48 GMT-0800 (PST)
4: enqueued: Thu Feb 01 2018 12:41:22 GMT-0800 (PST); dequeued: Thu Feb 01 2018 12:41:58 GMT-0800 (PST)

> i = 0
> for (var k = 0; k < 5; k++) setTimeout(() => f(), 1000*k);
0: enqueued: Thu Feb 01 2018 12:45:00 GMT-0800 (PST); dequeued: Thu Feb 01 2018 12:45:00 GMT-0800 (PST)
1: enqueued: Thu Feb 01 2018 12:45:01 GMT-0800 (PST); dequeued: Thu Feb 01 2018 12:45:10 GMT-0800 (PST)
3: enqueued: Thu Feb 01 2018 12:45:03 GMT-0800 (PST); dequeued: Thu Feb 01 2018 12:45:20 GMT-0800 (PST)
4: enqueued: Thu Feb 01 2018 12:45:04 GMT-0800 (PST); dequeued: Thu Feb 01 2018 12:45:30 GMT-0800 (PST)
2: enqueued: Thu Feb 01 2018 12:45:02 GMT-0800 (PST); dequeued: Thu Feb 01 2018 12:45:40 GMT-0800 (PST)

I can work around this limitation so this isn't an urgent issue for me. However if I'm misunderstanding something any pointers would be helpful. I have a slight suspicion that maybe setTimeout doesn't quite work the way I think it does :)

remove "type": "module" in `package.json`

Currently, "type": "module" is not supported well in webpack 5 or ts-node, in package.json it already has "exports" and "module" fields set properly, so no need the "type": "module" field.

same issue is also happening with the just-performance

How to use exactly

I am confused after reading the example:

var RateLimiter = require('limiter').RateLimiter;
var limiter = new RateLimiter(150, 'hour', true); // fire CB immediately

// Immediately send 429 header to client when rate limiting is in effect
limiter.removeTokens(1, function(err, remainingRequests) {
if (remainingRequests < 1) {
response.writeHead(429, {'Content-Type': 'text/plain;charset=UTF-8'});
response.end('429 Too Many Requests - your IP is being rate limited');
} else {
callMyMessageSendingFunction(...);
}
});

Where is 'response' defined? Don't we need to hook the limiter to a path with app.use()? How does the limiter know the incoming IP otherwise? Also, how do we queue up the messages to ensure correct order?

Please release new version to NPM

Hi!

I am running into a bug when using parentBucket, which is fixed here: 2b99def

Could you make a new version available on NPM so I can benefit from this fix?

Thanks!

QUESTION: cancel removeTokens()

I'm wondering how folks are gracefully cancelling/aborting calls to RateLimiter.removeTokens()?

Here's a sample use case where I'd want to cancel:

  1. User registers with my app to pull Fitbit data as it becomes available
  2. App adds a listener for notifications from Fitbit that new data is available
  3. App instantiates a subscription handler that queues inbound updates and throttles requests to FB via a RateLimiter instance):
class SubscriptionHandler {
  constructor(key, handlerFn) {
    this.key = key;
    this.handlerFn = handlerFn;
    this.rateLimiter = new limiter.RateLimiter(FITBIT_LIMIT_PER_USER_PER_HOUR, 'hour');
    this.queue = async.queue((notificationId, callback) => {
      this.rateLimiter.removeTokens(1, (error, remainingRequests) => {
        this._processNotification(notificationId, callback);
      });
    });

  enqueue(data, completionCallback) {
    this.queue.push(data.id);
  }

const myHandler = new SubscriptionHandler(myKey, myFBDataFetch);
  1. Upon receiving such a notification, the app enqueues its handling, which occurs when RateLimiter::removeTokens() successfully drains its token bucket.
  2. User de-registers before any number of enqueued notifications have been processed (correctly on-hold/pending by the rate limiter)
  3. ???

Step (5) is what's in question. I'd like to cancel the wait on removeTokens() and allow the handler instance to be garbage collected:

class SubscriptionHandler {
  constructor(key, handlerFn) {
    this.key = key;
    this.handlerFn = handlerFn;
    this.rateLimiter = new limiter.RateLimiter(FITBIT_LIMIT_PER_USER_PER_HOUR, 'hour');
    this.queue = async.queue((notificationId, callback) => {
      this.tokenTimeoutID = this.rateLimiter.removeTokens(1, (error, remainingRequests) => {
        this._processNotification(notificationId, callback);
      });
    });

  release() {
    // Maybe something like this?
    clearTimeout(this.tokenTimeoutID);
    delete this.rateLimiter;
    this.queue.kill();
    delete this.queue;
  }
}

myHandler.release();
delete myHandler;

Unfortunately, with the existing implementation of RateLimiter and its internal TokenBucket, there is no way to obtain the timeoutID assigned when TokenBucket invokes its internal comeBackLater() method. (See https://github.com/jhurliman/node-rate-limiter/blob/master/lib/tokenBucket.js, line 108). Thus, there is always an outstanding reference to the SubscriptionHandler instance, and i cannot get it garbage collected.

If the timer ID allocated by setTimeout() were returned from comeBackLater() (rather than the false value currently returned) and bubbled back out as the return value from RateLimiter::removeTokens(), then I could cancel it.

Obviously that breaks encapsulation, and perhaps the RateLimiter or its internal TokenBucket could keep rack of the timer ID and have methods for cancelling it, but I'm simply trying to get my idea across...

Given the above scenario, what would you recommend? @jhurliman ?

(Note that I prefer the semantics of the asynchronous removeTokens() method, and would rather not switch to my own periodic retry calling tryRemoveTokens()...)

Unexpected token '?'

I am running this example:

import { RateLimiter } from "limiter";

// Allow 150 requests per hour (the Twitter search limit). Also understands
// 'second', 'minute', 'day', or a number of milliseconds
const limiter = new RateLimiter({ tokensPerInterval: 150, interval: "hour" });

async function sendRequest() {
  // This call will throw if we request more than the maximum number of requests
  // that were set in the constructor
  // remainingRequests tells us how many additional requests could be sent
  // right this moment
  const remainingRequests = await limiter.removeTokens(1);
  callMyRequestSendingFunction(...);
}

and i am getting this error:

this.fireImmediately = fireImmediately ?? false;
                                                ^

SyntaxError: Unexpected token '?'

Example usage with limits per api user

I have the following situation,

  • Public api with multiple users. Each user has an apiKey that is validated on each request.
  • Each user has a limit of 200 requests per hour.

How would I configure node-rate-limiter for this setup?

How to use rate limiter and retrieve my business method result

Hi
I'm trying to use this library in order to limit number of http call my app is doing to and external provider.
So my rate limited method MUST provide an promise result (api result).

Business method example:

function getDocs() {
  return fetch("https://provider.com/api/docs")
}

I'm asking myself on how to do that with this library.

I will soon suggest a linked PR with some usage examples.

  • one blocking wait (seems not good practice),
  • one using async promise suggested by @sunknudsen in #63

I would like to know:

  • does current library could embed a samples/ directory with some ready-to-use examples,
  • what is the best way to handle and get promise result with this (waiting mode) rate limit?

Best regards

Browser support?

Hi jhurliman have you thought of making your code suitable to run within the web browser, too? Thanks.

G.

Will this work with what I need to do?

Hey there

I have a list of 100k emails. I'd like to only send 25 per second. I have setup the code like this:

var async = require('async');
var RateLimiter = require('limiter').RateLimiter;
var limiter = new RateLimiter(25, 'second');

var emails = [];

for (var i = 0; i < 100000; i++) {
    emails.push(i);
};

async.eachSeries(emails, function(email, nextEmail){

    limiter.removeTokens(1, function(err, remainingRequests) {
        asyncEmailFunction(email);
        return nextEmail();
    });

});

// Just as an example
function asyncEmailFunction(email) {
    console.log(email);
};

It works great until about 300 emails, then it just bogs down and doesn't complete them as fast.

Importing from esm in Node.js is broken in v2.1.0

Importing from esm in Node.js is broken in v2.1.0:

import { RateLimiter } from 'limiter'
         ^^^^^^^^^^^
SyntaxError: Named export 'RateLimiter' not found. The requested module 'limiter' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from 'limiter';
const { RateLimiter } = pkg;

    at ModuleJob._instantiate (node:internal/modules/esm/module_job:105:21)
    at async ModuleJob.run (node:internal/modules/esm/module_job:151:5)
    at async Loader.import (node:internal/modules/esm/loader:166:24)
    at async Object.loadESM (node:internal/process/esm_loader:68:5)

v2.0.1 was working.

Even distribution over time

If I specify 20 / minute, and I have thousands of requests per second (for example). Right now I see 20 events happen right away, then nothing for 1 minute, then another burst of 20 events. It would be nice to specify an option for an even distribution so that one event would fire every 3 seconds rather than bursting every minute.

Thanks!

Module and socket.io

Hi,

Don't know where to comment this. But how would node-rate-limited be implemented along with socket.io , how Would it be used to Throttle a client.id at sending events?

Hope I get an idea , would like to use your Module..

regards

Update npm package

#45 fixes a type declaration that can prevent typescript using this module from compiling, but the package has not been updated since this change.

Package should be updated to 1.1.3.

Rate Limit is violated in a sense if I remove 1 token every second when I define bucket interval as 1 minute

I implement a queue to hold requests until the condition t remove a token holds true, and if it does I remove a token and proceed with my task. I define my bucket size to be 3, and the rate limit interval as 1 minute. Given below is the code I used initially to implement rate limiter.

Throttler.prototype.checkAndExecute = function ()
  if (this.queue.length > 0) 
    if (this.limiter.getTokensRemaining() > 0)
      if (this.limiter.tryRemoveTokens(1)) {
        var job = that.queue.shift();
        if (job) {
          // do my job....
        }
      }
    }
  }
};

I wrap the above function inside an interval to check the queue contents and trigger the job if a token is available. Code given below -

this.peekInterval = setInterval( function() {
    that.checkAndExecute();
  }, 1000 );

The issue is that when I send 10 jobs to the queue, the first 3 are picked immediately by removing tokens from the bucket and executed. So, ideally, my request to remove 1 more token should not be allowed until at least 60 seconds have passed since I removed the first token. But, the bcuket gets filled up with another token in 20 seconds (60 seconds / 3), and the 4th job gets to execute in the 21st second. Thus, in the 1 minute interval since the first token was removed, more than 3 jobs got executed (in fact 5 jobs execute in the first minute itself), which violates the rate limit I set - 3 jobs per minute.

I am able to work around with this by modifying the checkAndExecute function in the way below -

Throttler.prototype.checkAndExecute = function () {
  if (that.queue.length > 0) {
    if (this.limiter.getTokensRemaining() >= this.bucketSize) {
      if (this.limiter.tryRemoveTokens(1)) {
        var job = that.queue.shift();
        if (job) {
          // do my job....
        }
      }
    }
  }
};

I'm not sure if anyone else has faced this scenario like I did, but just wanted to flag this out.

I understand that how the node rate-limiter and token-bucket functions holds good when a user wants to implement the token bucket algorithm, but in true definition of a rate limiter, the bucket should not be filled with additional tokens until the time interval since the removal of the 1st token has expired.

HTTP Limit headers

I would like to set the HTTP headers:

  • X-Rate-Limit-Limit: The number of allowed requests in the current period
  • X-Rate-Limit-Remaining: The number of remaining requests in the current period
  • X-Rate-Limit-Reset: The number of seconds left in the current period

something like this:

var RateLimiter = require('limiter').RateLimiter;
var limiter = new RateLimiter(150, 'hour', true);
console.log(limiter.limit()); // 150
console.log(limiter.reset()); // 58 (seconds elapsed)

Can I do something like that?

Rate Limiting does not happen for particular IP address

Limits = 40/day
Once 40 requests are served irrespective of the IP Addresses, It redirects to the 429 view.
So even the visitor whose visiting the site for the first time, is redirected to 429.
Can we have rate limiting for every IP address?

var RateLimiter = require('limiter').RateLimiter;
var limiter = new RateLimiter(40, 'day', true);  // fire CB immediately

app.use(function (req, res, next) {
  if ((req.headers["user-agent"].indexOf("Googlebot") != -1) || req.url == "/429")
    next();
  else
    limiter.removeTokens(1, function(err, remainingRequests) {
      if (remainingRequests < 0) {
        res.redirect('/429');
      } else {
        next();
      }
    });
});

removeTokens 1st parameter documentation

Documentation does not say anything. The 1st parameter of the removeTokens is the number of requests to be removed?

limiter.removeTokens(1, function(err, remainingRequests)

Question: Multiple limits

Is it possible to have two limits?
For example, I want to limit 1 request per second, to a maximum of 1000 per hour.

Can I do that using this package?

Feature req: chainable limiters, configuration functions and Promises

Hi,
I'd love for this library to have the following functionality, as it would help me a lot in the app I'm developing:

  • chainable rate limiters: like the parent parameter of TokenBucket, in order for the RateLimiter to obey both a per-second and per-hour limit
  • configuration options à la bottleneck: the ability to pause, stop, or reconfigure RateLimiter
  • promisification of the library (bluebird)

Would that be possible? If not, please let me know and I'll see if I can add them.

Thanks!

Set current tokens remaining in constructor

In my case, I create the limiter object after the first call using the returned header.

So for now I create the object, then run a removeTokens function and ignore the result. It would be really fantastic if I could simply pass the remaining tokens for this interval into the constructor, that way I wouldn't have to make a simply removeTokens method with a blank callback.

Such as Limiter(120, 'minute', 119);

Add non-leaky bucket implementation

Hello, I think it may be a good idea to include a non-leaky bucket implementation. Instead of it eventually doing the intended action, just block the action altogether.

getTokensRemaining() returns unexpected values

I've been using this package for years to provide rate limiting for a chat application and it has worked really well for me. I use a limiter for message rate (flood protection) and when the limiter has <= 50% tokens remaining, the server should send a warning message to the user.

Some users have always been able to trigger flood protection w/o getting any warning from the server and I have finally figured out why. Please consider the following code:

const RateLimiter = require('limiter').RateLimiter

const limiter = new RateLimiter(10, 'minute')
const remove = () => {
  let date = new Date()
  let remaining

  if (limiter.tryRemoveTokens(1)) {
    remaining = limiter.getTokensRemaining()
    console.log(`${date} - token removed, remaining: ${remaining}`)
  } else {
    console.log(`${date} - NO tokens remaining`)
  }
}

remove()

setInterval(() => {
  remove()
}, 3500)

Note the interval of 3.5 seconds.

This is the output:

Sat May 04 2019 17:05:03 GMT-0400 (EDT) - token removed, remaining: 9
Sat May 04 2019 17:05:07 GMT-0400 (EDT) - token removed, remaining: 8.584
Sat May 04 2019 17:05:10 GMT-0400 (EDT) - token removed, remaining: 8.168
Sat May 04 2019 17:05:14 GMT-0400 (EDT) - token removed, remaining: 7.751999999999999
Sat May 04 2019 17:05:17 GMT-0400 (EDT) - token removed, remaining: 7.336166666666665
Sat May 04 2019 17:05:21 GMT-0400 (EDT) - token removed, remaining: 6.920166666666665
Sat May 04 2019 17:05:24 GMT-0400 (EDT) - token removed, remaining: 6.504166666666665
Sat May 04 2019 17:05:28 GMT-0400 (EDT) - token removed, remaining: 6.088166666666664
Sat May 04 2019 17:05:31 GMT-0400 (EDT) - token removed, remaining: 5.672166666666664
Sat May 04 2019 17:05:35 GMT-0400 (EDT) - token removed, remaining: 5.255999999999998
Sat May 04 2019 17:05:38 GMT-0400 (EDT) - NO tokens remaining
[SNIP]
Sat May 04 2019 17:06:06 GMT-0400 (EDT) - token removed, remaining: 9

You can see here that getTokensRemaining() never returns a value below 50% of the bucket size at this interval. My expectation is that remaining tokens would decrement 10, 9, 8, 7 etc. so I'm really confused why this is happening. Even though 10 tokens were removed, getTokensRemaining() reported 5.255999999999998 when it should have reported 0 (at least in my mind).

Plus I might be confused as to how this works, but I would expect tokens to be slowly added back to the bucket over time. What appears to be happening here is that about a minute after the first removal, the bucket is filled back up with 10 tokens.

I've been digging through tickets and the code but I can't quite figure out what's happening here and what I can do about it. This seems like it might be a bug so I thought I'd report it.

Any way to force a task to the front of the queue?

I have to limit requests by user. Let's say a user has a queue with 10 background tasks lined up. Is there a way to push a single manual request to the top of the queue so it get's executed next before the rest of the background jobs?

Got ERR_REQUIRE_ESM

Hi,
I got this message

Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: …/limiter/dist/cjs/index.js
require() of ES modules is not supported.
require() of …/limiter/dist/cjs/index.js from MyService.js is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules.

When trying to require
const RateLimiter = require('limiter').RateLimiter;

Node: v15.14.0
Limiter: 2.0.1
Note that I previously used v1.1.3 and this does not happen.

'process' is not defined

After migrating to webpack 5 the call of the removeTokens methods crashes the app with the error process is not defined

import { RateLimiter } from 'limiter';

const maxRequestsPerSecond = 10;
const myLimiter = new RateLimiter(maxRequestsPerSecond, 'second');

const someMethod = async () => {
  await new Promise<void>(resolve => {
    myLimiter.removeTokens(1, () => resolve());
  }
}

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../node_modules/limiter/dist/esm/RateLimiter'

Not sure if this is a case of "pilot error" but after upgrading this package and switching to 'import' from 'require' in my code, I'm getting the following error (when I try to run my tests with mocha):

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/Volumes/RAID1/projects/threeceeUtilities/node_modules/limiter/dist/esm/RateLimiter' imported from /Volumes/RAID1/projects/threeceeUtilities/node_modules/limiter/dist/esm/index.js
at finalizeResolution (internal/modules/esm/resolve.js:276:11)
at moduleResolve (internal/modules/esm/resolve.js:699:10)
at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:810:11)
at Loader.resolve (internal/modules/esm/loader.js:86:40)
at Loader.getModuleJob (internal/modules/esm/loader.js:230:28)
at ModuleWrap. (internal/modules/esm/module_job.js:56:40)
at link (internal/modules/esm/module_job.js:55:36)

I'm importing as in the README:

import { RateLimiter } from "limiter";

I've added the most recent version ([email protected]) using 'yarn add limiter'

Am I doing something wrong? Any help appreciated

Rate limit by Parameter

I'm assuming this is by IP? Is there anyway I can insert via the options like a parameter to use instead?

For example right now it is set up to not allow more then 5 requests per IP address per minute. But right now I'm hoping to make it by email.

Would this project be open to a PR adding asyncRemoveTokens?

I have been wrapping removeTokens in a promise (as suggested in #52) for a while.

Feels like it would be clean and useful if we implemented an asyncRemoveTokens method.

function asyncRemoveTokens(count: number, rateLimiter: RateLimiter) {
  return new Promise((resolve, reject) => {
    rateLimiter.removeTokens(count, (error, remainingRequests) => {
      if (error) return reject(error)
      resolve(remainingRequests)
    })
  })
}

Invalid period strings should be rejected

If you use an invalid period string, eg. because of a typo (new RateLimiter(10, "horu")), the library will fallback to "second", rather than throwing an error. This fallback is completely unexpected, and can cause outright wrong behaviour in the event of typos (as just happened to me).

Is it possible to use async function as callback?

I used await inside callMyRequestSendingFunction(...). Is it possible to use async function as callback?
As I tried adding async,

while (morePagesAvailable) {
        let response:any = [];
        myRateLimiter.removeTokens(1, async function(err, remainingRequests) {
            response = await fetcher(url);     
       });

       // Add response to the list of responses
}

npm package name conflict

This is what I was looking for however it can't currently be installed with npm since there is already such a package.

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.