Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache TTL is less than :period #480

Closed
parov opened this issue May 4, 2020 · 3 comments
Closed

Cache TTL is less than :period #480

parov opened this issue May 4, 2020 · 3 comments

Comments

@parov
Copy link

parov commented May 4, 2020

Looking at how the cache keys are set when using throttling, I've figured out that, depending on the time in which RackAttack is called, the TTL for the cache keys is set to a value lower than the :period.

Testing the code

Rack::Attack.throttle("logins/ip/2_60", :limit => 2, :period => 60.seconds) do |request|
    request.ip
end

is showing the following behaviour:

1588159072.159450 [0 127.0.0.1:53269] "get" "rack::attack:26469317:logins/ip/2_60:127.0.0.1"
1588159072.160245 [0 127.0.0.1:53269] "setex" "rack::attack:26469317:logins/ip/2_60:127.0.0.1" "9" "1"
1588159077.259167 [0 127.0.0.1:53269] "get" "rack::attack:26469317:logins/ip/2_60:127.0.0.1"
1588159077.259842 [0 127.0.0.1:53269] "incrby" "rack::attack:26469317:logins/ip/2_60:127.0.0.1" "1"
1588159085.831212 [0 127.0.0.1:53269] "get" "rack::attack:26469318:logins/ip/2_60:127.0.0.1"
1588159085.831920 [0 127.0.0.1:53269] "setex" "rack::attack:26469318:logins/ip/2_60:127.0.0.1" "56" "1"
1588159089.522911 [0 127.0.0.1:53269] "get" "rack::attack:26469318:logins/ip/2_60:127.0.0.1"
1588159089.523468 [0 127.0.0.1:53269] "incrby" "rack::attack:26469318:logins/ip/2_60:127.0.0.1" "1"
1588159092.728102 [0 127.0.0.1:53269] "get" "rack::attack:26469318:logins/ip/2_60:127.0.0.1"
1588159092.728851 [0 127.0.0.1:53269] "incrby" "rack::attack:26469318:logins/ip/2_60:127.0.0.1" "1"

What I see is that:

  • after the first successful call, the throttle cache key is with TTL of 9 seconds
  • I then make a second successful call in between the 9 seconds
  • the key then expires, and I'm able to do another 2 successful calls in the next 56 seconds
  • the last call gets finally blocked by RackAttak

Given the original period of 60 seconds, I was able to successfully requests the resource twice as expected.
While the behaviour can be acceptable when using a single rule, it gets a bit more concerning when using exponential backoff, because it becomes harder to keep control on the amount of requests that should be blocked.

For example, for a limit of 20 requests in 7200 seconds, I get a TTL of 1710 seconds

"setex" "rack::attack:52938663:logins/ip/6_30:127.0.0.1" "30" "1"
"setex" "rack::attack:7218908:logins/ip/10_220:127.0.0.1" "90" "1"
"setex" "rack::attack:1323466:logins/ip/15_1200:127.0.0.1" "510" "1"
"setex" "rack::attack:220577:logins/ip/20_7200:127.0.0.1" "1710" "1"

What's the reason behind calculating the TTL subtracting the current time?

@peter-roland-toth
Copy link

I believe the reason for this is that the key used for caching specifies a timeframe that doesn't necessarily starts when you make your first request.

Let's consider your example, where you specify a period of 60 seconds. If you make a request at the timestamp 1588159072, the key will contain 1588159072 / 60, which is 26469317 (you can also see this in the logs you attached). In fact, all timestamps between 1588159020 - 1588159079 will receive the same key. And I think the TTL is being set so that it matches the end of the timeframe where you would get the same key, plus an extra second, as described here: #85. In your case, the next timeframe would start at 1588159080, since 1588159080 / 60 == 26469318, and your TTL is 9 seconds which is 1588159080 + 1.

Indeed, the downside of this approach is that the limit you specify will be applied only to these "blocks" of time. It's guaranteed that you can make n requests between t0 and t0 + period, where t0 is a timestamp satisfying t0 % period == 0, but it's not guaranteed that the limit will also be applied where t0 is not satisfying this equality. More concretely, in your case you could have made 2 requests at 1588159079, and another 2 requests just a second later, because that would have been a different time block. So even if you are specifying a limit of 2 requests per minute, it's possible to make 4 requests in 2 seconds. In fact, this is the upper limit, the user won't be able to make more than 2n requests in any possible timeframe.

I think the only solution to strictly apply the limit to all possible timeframes would be to record the timestamp of every request made by a user, but that would require much more storage and also some extra processing.

I hope this helps!

@grzuy
Copy link
Collaborator

grzuy commented May 23, 2020

I believe the reason for this is that the key used for caching specifies a timeframe that doesn't necessarily starts when you make your first request.

Indeed.

Thanks @peter-roland-toth for responding!

@grzuy grzuy closed this as completed May 23, 2020
@grzuy
Copy link
Collaborator

grzuy commented May 23, 2020

For what is worth...

There's been an attempt in the past to make the throttle algorithm more sophisticated in #206, in case anyone wants to take another shot at trying.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants