The Redis INCR docs have some examples of using that command for a Rate Limiter based on IP addresses.
For instance, maintain a counter with a time-window as key, and INCR
/EXPIRE
it in a single transaction via MULTI
/EXEC
:
ts = CURRENT_UNIX_TIME()
keyname = ip+":"+ts
MULTI
INCR(keyname)
EXPIRE(keyname,10)
EXEC
A problem with this approach is having to maintain a time window in the counter key, and potentially having multiple keys for a single ip address. The docs then state:
An alternative implementation uses a single counter, but is a bit more complex to get it right without race conditions.
Why is it complex to get this right? The following implementation seems simpler than the former:
MULTI
INCR(ip)
EXPIRE(ip, 1, NX)
EXEC
The strategy here is simply to add NX
, ensuring that EXPIRE
only acts if there is no expiration yet. We also set the expiration to 1 second to match the desired rate limiting period. Since this is performed in a single transaction, we're ensured to call EXPIRE
after INCR
.
So, where is the race condition in this approach?
Note: There are already some questions about why a client sending 2 separate transactions (one for INCR
and one for EXPIRE
) would face a race condition (or, more precisely, drop before EXPIRE
). This question is specifically for the case where both operations are performed in a single transaction via MULTI
/EXEC
.