rolling-rate-limiter
Advanced tools
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(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
22287
484
134