How to implement rate limiting using Redis
You could switch from "5 requests in the last minute" to "5 requests in minute x". By this it would be possible to do:
counter = current_time # for example 15:03
count = INCR counter
EXPIRE counter 60 # just to make sure redis doesn't store it forever
if count > 5
print "Exceeded the limit"
If you want to keep using "5 requests in the last minute", then you could do
counter = Time.now.to_i # this is Ruby and it returns the number of milliseconds since 1/1/1970
key = "counter:" + counter
INCR key
EXPIRE key 60
number_of_requests = KEYS "counter"*"
if number_of_requests > 5
print "Exceeded the limit"
If you have production constraints (especially performance), it is not advised to use the KEYS
keyword. We could use sets instead:
counter = Time.now.to_i # this is Ruby and it returns the number of milliseconds since 1/1/1970
set = "my_set"
SADD set counter 1
members = SMEMBERS set
# remove all set members which are older than 1 minute
members {|member| SREM member if member[key] < (Time.now.to_i - 60000) }
if (SMEMBERS set).size > 5
print "Exceeded the limit"
This is all pseudo Ruby code, but should give you the idea.
This is an old question that was already answered, but here's an implementation I did taking some inspiration from here. I'm using ioredis for Node.js
Here is the rolling-window time limiter in all its asynchronous yet race-condition-free (I hope) glory:
var Ioredis = require('ioredis');
var redis = new Ioredis();
// Rolling window rate limiter
//
// key is a unique identifier for the process or function call being limited
// exp is the expiry in milliseconds
// maxnum is the number of function calls allowed before expiry
var redis_limiter_rolling = function(key, maxnum, exp, next) {
redis.multi([
['incr', 'limiter:num:' + key],
['time']
]).exec(function(err, results) {
if (err) {
next(err);
} else {
// unique incremented list number for this key
var listnum = results[0][1];
// current time
var tcur = (parseInt(results[1][1][0], 10) * 1000) + Math.floor(parseInt(results[1][1][1], 10) / 1000);
// absolute time of expiry
var texpiry = tcur - exp;
// get number of transacation in the last expiry time
var listkey = 'limiter:list:' + key;
redis.multi([
['zadd', listkey, tcur.toString(), listnum],
['zremrangebyscore', listkey, '-inf', texpiry.toString()],
['zcard', listkey]
]).exec(function(err, results) {
if (err) {
next(err);
} else {
// num is the number of calls in the last expiry time window
var num = parseInt(results[2][1], 10);
if (num <= maxnum) {
// does not reach limit
next(null, false, num, exp);
} else {
// limit surpassed
next(null, true, num, exp);
}
}
});
}
});
};
and here is a kind of lockout-style rate limiter:
// Lockout window rate limiter
//
// key is a unique identifier for the process or function call being limited
// exp is the expiry in milliseconds
// maxnum is the number of function calls allowed within expiry time
var util_limiter_lockout = function(key, maxnum, exp, next) {
// lockout rate limiter
var idkey = 'limiter:lock:' + key;
redis.incr(idkey, function(err, result) {
if (err) {
next(err);
} else {
if (result <= maxnum) {
// still within number of allowable calls
// - reset expiry and allow next function call
redis.expire(idkey, exp, function(err) {
if (err) {
next(err);
} else {
next(null, false, result);
}
});
} else {
// too many calls, user must wait for expiry of idkey
next(null, true, result);
}
}
});
};
Here's a gist of the functions. Let me know if you see any issues.
Note: The following code is a sample implementation in Java.
private final String COUNT = "count";
@Autowired
private StringRedisTemplate stringRedisTemplate;
private HashOperations hashOperations;
@PostConstruct
private void init() {
hashOperations = stringRedisTemplate.opsForHash();
}
@Override
public boolean isRequestAllowed(String key, long limit, long timeout, TimeUnit timeUnit) {
Boolean hasKey = stringRedisTemplate.hasKey(key);
if (hasKey) {
Long value = hashOperations.increment(key, COUNT, -1l);
return value > 0;
} else {
hashOperations.put(key, COUNT, String.valueOf(limit));
stringRedisTemplate.expire(key, timeout, timeUnit);
}
return true;
}
The canonical way to do rate limiting is via the Leaky bucket algorithm. The downside of using a counter, is that a user can perform a bunch of request right after the counter is reset, i.e. 5 actions in the first second of the next minute for your case. The Leaky bucket algorithm solves this problem. Briefly, you can used ordered sets to store your "leaky bucket", using action time stamps as keys to fill it.
Check out this article for the exact implementation: Better Rate Limiting With Redis Sorted Sets
UPDATE:
There is also another algorithm, which has some advantages compared to leaky bucket. It's called Generic Cell Rate Algorithm . Here's how it works at the higher level, as described in Rate Limiting, Cells, and GCRA:
GCRA works by tracking remaining limit through a time called the “theoretical arrival time” (TAT), which is seeded on the first request by adding a duration representing its cost to the current time. The cost is calculated as a multiplier of our “emission interval” (T), which is derived from the rate at which we want the bucket to refill. When any subsequent request comes in, we take the existing TAT, subtract a fixed buffer representing the limit’s total burst capacity from it (τ + T), and compare the result to the current time. This result represents the next time to allow a request. If it’s in the past, we allow the incoming request, and if it’s in the future, we don’t. After a successful request, a new TAT is calculated by adding T.
There is a redis module that implements this algorithm available on GitHub: https://github.com/brandur/redis-cell