skip to Main Content

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.

3

Answers


  1. 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;
        }
    
    Login or Signup to reply.
  2. 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
    
    Login or Signup to reply.
  3. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search