stkUser
stkUser

Reputation: 125

Redis Rate Limiter Pattern

I am trying to use Redis Rate limiting patter as specified in https://redis.io/commands/incr under "Pattern: Rate limiter 1". But how can I scale this in case I want to do rate limiting across multiple servers. Like I have service deployed across 5 servers behind load balancer and I want total requests per api key across 5 servers should not cross x/sec. As per redis pattern I mentioned , the problem is that if I have my rate limiter running in multiple servers itself, then the two different request to two different rate limiter servers,can do "get key" at same time and read same value, before anyone updates it, which can probably allow more requests to go.How can I handle this?I can obviously put get in MULTI block, but I think it will make things a lot more slow.

Upvotes: 0

Views: 2311

Answers (3)

Hank Liu
Hank Liu

Reputation: 9

The accepted answer is incorrect. It leads to inaccurate counter value. Let's look a the following example:

5 clients executing 5 concurrent requests to Redis. The current state of the counter is 10, and while the limit is also 10.

The 5 concurrent requests will increment the counter to 15 while declining each of the requests. Rather, the value should remain 10 to reflect on the correct number of times clients were "allowed".

Solution: we effectively needs to combine two separate atomic operations into a single atomic operation. That's where LUA script comes in. It's simply a modification on Redis it self to introduce another code path that "executes a get, and then a set" atomically. And it does so because Redis is single thread.

Upvotes: 0

sazzad
sazzad

Reputation: 6267

INCR replies with the updated value. So it can be used as both write and read command.

FUNCTION LIMIT_API_CALL(ip)
ts = CURRENT_UNIX_TIME()
keyname = ip+":"+ts

MULTI
    INCR(keyname)
    EXPIRE(keyname,10)
EXEC

current = RESPONSE_OF_INCR_WITHIN_MULTI
IF current > 10 THEN
    ERROR "too many requests per second"
ELSE
    PERFORM_API_CALL()
END

Upvotes: 3

sonus21
sonus21

Reputation: 5398

You need to run LUA script that will check rate-limiting and increase/decrease/reset the counter(s).

You can find a simple example in Larval framework here

https://github.com/laravel/framework/blob/8.x/src/Illuminate/Redis/Limiters/DurationLimiter.php

 /**
     * Get the Lua script for acquiring a lock.
     *
     * KEYS[1] - The limiter name
     * ARGV[1] - Current time in microseconds
     * ARGV[2] - Current time in seconds
     * ARGV[3] - Duration of the bucket
     * ARGV[4] - Allowed number of tasks
     *
     * @return string
     */
    protected function luaScript()
    {
        return <<<'LUA'
local function reset()
    redis.call('HMSET', KEYS[1], 'start', ARGV[2], 'end', ARGV[2] + ARGV[3], 'count', 1)
    return redis.call('EXPIRE', KEYS[1], ARGV[3] * 2)
end
if redis.call('EXISTS', KEYS[1]) == 0 then
    return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1}
end
if ARGV[1] >= redis.call('HGET', KEYS[1], 'start') and ARGV[1] <= redis.call('HGET', KEYS[1], 'end') then
    return {
        tonumber(redis.call('HINCRBY', KEYS[1], 'count', 1)) <= tonumber(ARGV[4]),
        redis.call('HGET', KEYS[1], 'end'),
        ARGV[4] - redis.call('HGET', KEYS[1], 'count')
    }
end
return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1}
LUA;
    }

Upvotes: 2

Related Questions