Skip to content

Instantly share code, notes, and snippets.

@RobThree
Last active December 16, 2019 21:34
Show Gist options
  • Save RobThree/7786872 to your computer and use it in GitHub Desktop.
Save RobThree/7786872 to your computer and use it in GitHub Desktop.
Simple, rude, rate limiter (using memcache).
<?php
/* Very simple, crude, rate limiter */
/*
Assumes _MEMCACHEDSITEPREFIX, _MEMCACHEDHOST and _MEMCACHEDPORT are `defined`:
define('_MEMCACHEDHOST', '127.0.0.1');
define('_MEMCACHEDPORT', 11211);
define('_MEMCACHEDSITEPREFIX', 'mysite');
*/
class RateLimiter {
private $rate_limit_cycle; //Seconds
private $cycle_req_quota; //Reqs/cycle
function RateLimiter($rate_limit_cycle = 86400, $cycle_req_quota = 5000) {
$this->rate_limit_cycle = $rate_limit_cycle;
$this->cycle_req_quota = $cycle_req_quota;
}
public function IsLimited($key, $prolong_on_rate_limit_exceeded = false) {
$memcache_id = _MEMCACHEDSITEPREFIX . '.ratelimit.' . $key;
$memcache = new Memcache();
$memcache->connect(_MEMCACHEDHOST, _MEMCACHEDPORT);
$current_hits = intval($memcache->get($memcache_id));
if ($current_hits >= $this->cycle_req_quota) {
if ($prolong_on_rate_limit_exceeded) {
//When rate limit is hit we prolong the key to the rate_limit_cycle number of seconds ("extra punishment")
$memcache->set($memcache_id, $this->cycle_req_quota, 0, $this->rate_limit_cycle);
}
return true;
}
if ($current_hits == 0)
$memcache->set($memcache_id, 1, 0, $this->rate_limit_cycle);
else
$memcache->increment($memcache_id);
return false;
}
//Returns if the request is rate limited; returning an error etc. is up to you
public static function CheckLimit($key, $rate_limit_cycle = 86400, $cycle_req_quota = 5000, $prolong_on_rate_limit_exceeded = false) {
$r = new RateLimiter($rate_limit_cycle, $cycle_req_quota, $prolong_on_rate_limit_exceeded);
return $r->IsLimited($key);
}
//Checks the rate limit for a specific key and exits with some HTTP headers when the rate limit is hit
public static function CheckLimitExit($key, $rate_limit_cycle = 86400, $cycle_req_quota = 5000, $prolong_on_rate_limit_exceeded = false) {
if (RateLimiter::CheckLimit($key, $rate_limit_cycle, $cycle_req_quota, $prolong_on_rate_limit_exceeded)) {
header('HTTP/1.1 429 Too Many Requests');
header(sprintf('Retry-After: %d', $rate_limit_cycle));
header('Connection: close');
echo '<h1>Too many requests</h1>';
exit();
}
}
//RateLimiter::CheckLimitByIPExit(); //5000 requests per day
//RateLimiter::CheckLimitByIPExit(null, 10, 5); //5 requests per 10 seconds
//RateLimiter::CheckLimitByIPExit("somemethodname", 10, 5); //Same as above, this time for a specific method
public static function CheckLimitByIPExit($key = null, $rate_limit_cycle = 86400, $cycle_req_quota = 5000, $prolong_on_rate_limit_exceeded = false) {
RateLimiter::CheckLimitExit((isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null) . $key, $rate_limit_cycle, $cycle_req_quota, $prolong_on_rate_limit_exceeded);
}
}
?>
@drbraden
Copy link

drbraden commented Aug 2, 2018

Bet you didn't expect a comment on this! I just used a lightly modified version of this snippet in a project :)

Turns out that the value of $prolong_on_rate_limit_exceeded is not being respected. The static function CheckLimit() is creating a new RateLimiter and passing in $prolong_on_rate_limit_exceeded as a 3rd argument, but the constructor doesn't take a 3rd argument. As written, $prolong_on_rate_limit_exceeded is expected to be passed into IsLimited(), but it is not.

What I did was to add $prolong_on_rate_limit_exceeded as an optional parameter to the constructer, like it is with $rate_limit_cycle and $cycle_req_quota:

    function RateLimiter($rate_limit_cycle = 86400, $cycle_req_quota = 5000, $prolong_on_rate_limit_exceeded = false) {
        $this->rate_limit_cycle = $rate_limit_cycle;
        $this->cycle_req_quota = $cycle_req_quota;
        $this->prolong_on_rate_limit_exceeded = $prolong_on_rate_limit_exceeded;
    }

I then changed IsLimited()'s declaration to remove the $prolong_on_rate_limit_exceeded argument, and the function body to reference the class variable instead: $this->prolong_on_rate_limit_exceeded.

Thanks for posting this 😃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment