Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

rolling-rate-limiter

Package Overview
Dependencies
Maintainers
1
Versions
32
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

rolling-rate-limiter - npm Package Compare versions

Comparing version 0.0.3 to 0.1.0

37

index.js

@@ -17,3 +17,3 @@ var assert = require("assert");

interval *= 1000;
minDifference *= 1000;
minDifference = minDifference ? 1000*minDifference : null;

@@ -46,8 +46,16 @@ if (!options.redis) {

var oldMembers = resultArr[2].map(Number);
var tooManyActionsInInterval = oldMembers.length >= maxInInterval;
var previousActionTooRecent = minDifference && (now - oldMembers.pop() < minDifference);
var userSet = resultArr[2].map(Number);
return cb(null, !tooManyActionsInInterval && !previousActionTooRecent);
var tooManyInInterval = userSet.length >= maxInInterval;
var timeSinceLastRequest = minDifference && (now - userSet[userSet.length - 1]);
var result;
if (tooManyInInterval || timeSinceLastRequest < minDifference) {
result = Math.min(userSet[0] - now + interval, minDifference ? minDifference - timeSinceLastRequest : Infinity);
result = Math.floor(result/1000); // convert from microseconds.
} else {
result = 0;
}
return cb(null, result)
});

@@ -72,9 +80,17 @@ }

clearTimeout(timeouts[id]);
var userStorage = storage[id] = (storage[id] || []).filter(function(timestamp) {
var userSet = storage[id] = (storage[id] || []).filter(function(timestamp) {
return timestamp > clearBefore;
});
var tooManyActionsInInterval = userStorage.length >= maxInInterval;
var previousActionTooRecent = minDifference && (now - userStorage[userStorage.length - 1] < minDifference);
userStorage.push(now);
var tooManyInInterval = userSet.length >= maxInInterval;
var timeSinceLastRequest = minDifference && (now - userSet[userSet.length - 1]);
var result;
if (tooManyInInterval || timeSinceLastRequest < minDifference) {
result = Math.min(userSet[0] - now + interval, minDifference ? minDifference - timeSinceLastRequest : Infinity);
result = Math.floor(result/1000); // convert from microseconds.
} else {
result = 0;
}
userSet.push(now);
timeouts[id] = setTimeout(function() {

@@ -84,3 +100,2 @@ delete storage[id];

var result = !tooManyActionsInInterval && !previousActionTooRecent
if (cb) {

@@ -87,0 +102,0 @@ return process.nextTick(function() {

{
"name": "rolling-rate-limiter",
"version": "0.0.3",
"version": "0.1.0",
"description": "Rate limiter that supports a rolling window, either in-memory or backed by redis",

@@ -5,0 +5,0 @@ "main": "index.js",

@@ -5,4 +5,8 @@ # Rolling Rate Limiter

## Description
This is an implementation of a rate limiter in node.js that allows for rate limiting with a rolling window. 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 many existing implementations, in which a user could make 5 requests at 0:59 and another 5 requests at 1:01. The implementation uses what I believe to be a novel algorithm, using sorted sets. 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 without race conditions.
This is an implementation of a rate limiter in node.js that allows for rate limiting with a rolling window.
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 many existing implementations, in which a user could make 5 requests at 0:59 and another 5 requests at 1:01.
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 without race conditions. The implementation uses what I believe to be a novel algorithm, with sorted sets.
## Examples

@@ -33,8 +37,14 @@

// If none is provided, the limiter will not differentiate between users.
var timeLeft = limiter(userId)
var ok = limiter(userId)
if (ok) {
if (timeLeft > 0) {
// limit was exceeded, action should not be allowed
// timeLeft is the number of ms until the next action will be allowed
// note that this can be treated as a boolean, since 0 is falsy
} else {
// limit was not exceeded, action should be allowed
} else {
// limit was exceeded, action should not be allowed
}

@@ -75,6 +85,6 @@

function attemptAction(userId, cb) {
limiter(userId, function(err, ok) {
limiter(userId, function(err, timeLeft) {
if (err) {
// redis failed or similar.
} else if (!ok) {
} else if (timeLeft) {
// limit was exceeded, action should not be allowed

@@ -92,3 +102,2 @@ } else {

```javascript
var limiter = RateLimiter({

@@ -105,7 +114,9 @@ redis: redisClient,

// "req.ipAddress" could be replaced with any unique user identifier
limiter(req.ipAddress, function(err, success) {
// Note that the limiter returns the number of miliseconds until an action
// will be allowed. Since 0 is falsey, this can be treated as a boolean.
limiter(req.ipAddress, function(err, timeLeft) {
if (err) {
return res.status(500).send();
} else if (!success) {
return res.status(429).send();
} else if (timeLeft) {
return res.status(429).send("You must wait " + timeLeft + " ms before you can make requests.");
} else {

@@ -117,3 +128,2 @@ return next();

});
```

@@ -120,0 +130,0 @@

@@ -26,4 +26,4 @@ var sinon = require("sinon");

if (cb) {
limit(function(err, success) {
if (success) {
limit(function(err, blocked) {
if (!blocked) {
counts[userId]++;

@@ -34,4 +34,4 @@ }

} else {
var success = limit();
if (success) {
var blocked = limit();
if (!blocked) {
counts[userId]++;

@@ -159,2 +159,29 @@ }

it("returns the time after which actions will be allowed", function() {
var limiter1 = RateLimiter({
interval: 10000,
maxInInterval: 2
});
var first = limiter1();
var second = limiter1();
var third = limiter1();
expect(first).to.equal(0);
expect(second).to.equal(0);
expect(third).to.be.above(9900);
expect(third).to.be.below(10001);
var limiter2 = RateLimiter({
interval: 10000,
maxInInterval: 100,
minDifference: 100
});
first = limiter2();
second = limiter2();
expect(first).to.equal(0);
expect(second).to.be.above(90);
expect(second).to.be.below(101);
});
});

@@ -399,3 +426,34 @@

it("returns the time after which actions will be allowed", function(done) {
var limiter1 = RateLimiter({
redis: redis.createClient(),
interval: 10000,
maxInInterval: 2
});
async.times(3, function(n, next) {
limiter1(next);
}, function(err, results) {
expect(results[0]).to.equal(0);
expect(results[1]).to.equal(0);
expect(results[2]).to.be.above(9900);
expect(results[2]).to.be.below(10001);
// ---
var limiter2 = RateLimiter({
interval: 10000,
maxInInterval: 100,
minDifference: 100
});
async.times(3, function(n, next) {
limiter2(next);
}, function(err, results) {
expect(results[0]).to.equal(0);
expect(results[1]).to.be.above(90);
expect(results[1]).to.be.below(101);
done();
});
});
});
});
});
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc