Skip to content

Instantly share code, notes, and snippets.

@freekrai
Last active April 21, 2024 00:42
Show Gist options
  • Star 32 You must be signed in to star a gist
  • Fork 13 You must be signed in to fork a gist
  • Save freekrai/cdcd6ebb29d84b9dc244282e64caf5fe to your computer and use it in GitHub Desktop.
Save freekrai/cdcd6ebb29d84b9dc244282e64caf5fe to your computer and use it in GitHub Desktop.
PHP session-based rate limiter for APIs
<?php
date_default_timezone_set('America/Los_Angeles');
session_start();
include("ratelimiter.php");
// in this sample, we are using the originating IP, but you can modify to use API keys, or tokens or what-have-you.
$rateLimiter = new RateLimiter($_SERVER["REMOTE_ADDR"]);
$limit = 100; // number of connections to limit user to per $minutes
$minutes = 1; // number of $minutes to check for.
$seconds = floor($minutes * 60); // retry after $minutes in seconds.
try {
$rateLimiter->limitRequestsInMinutes($limit, $minutes);
} catch (RateExceededException $e) {
header("HTTP/1.1 429 Too Many Requests");
header(sprintf("Retry-After: %d", $seconds));
$data = 'Rate Limit Exceeded ';
die (json_encode($data));
}
// ok, they were within their limit, so let's continue with our app....
$data = "Data Returned from API ";
header('Content-Type: application/json');
die(json_encode($data));
<?php
/*
This is a really simple plug and play rate limiter class meant to be used with APIs.
Using sessions means we can throw this into any PHP API quickly.
$token can be anything to uniquely identify a user, either an IP address, an API key, a JWT token, anything that is unique to a single user.
$prefix can be whatever you want, we default it to "rate"
On init, we create a md5 hash of our $prefix and our $token, this becomes the prefix throughout the class.
We then append a timestamp to this prefix
*/
class RateExceededException extends Exception {}
class RateLimiter {
private $prefix;
public function __construct($token, $prefix = "rate") {
$this->prefix = md5($prefix . $token);
if( !isset($_SESSION["cache"]) ){
$_SESSION["cache"] = array();
}
if( !isset($_SESSION["expiries"]) ){
$_SESSION["expiries"] = array();
}else{
$this->expireSessionKeys();
}
}
public function limitRequestsInMinutes($allowedRequests, $minutes) {
$this->expireSessionKeys();
$requests = 0;
foreach ($this->getKeys($minutes) as $key) {
$requestsInCurrentMinute = $this->getSessionKey($key);
if (false !== $requestsInCurrentMinute) $requests += $requestsInCurrentMinute;
}
if (false === $requestsInCurrentMinute) {
$this->setSessionKey( $key, 1, ($minutes * 60 + 1) );
} else {
$this->increment($key, 1);
}
if ($requests > $allowedRequests) throw new RateExceededException;
}
private function getKeys($minutes) {
$keys = array();
$now = time();
for ($time = $now - $minutes * 60; $time <= $now; $time += 60) {
$keys[] = $this->prefix . date("dHi", $time);
}
return $keys;
}
private function increment( $key, $inc){
$cnt = 0;
if( isset($_SESSION['cache'][$key]) ){
$cnt = $_SESSION['cache'][$key];
}
$_SESSION['cache'][$key] = $cnt + $inc;
}
private function setSessionKey( $key, $val, $expiry ){
$_SESSION["expiries"][$key] = time() + $expiry;
$_SESSION['cache'][$key] = $val;
}
private function getSessionKey( $key ){
return isset($_SESSION['cache'][$key]) ? $_SESSION['cache'][$key] : false;
}
private function expireSessionKeys() {
foreach ($_SESSION["expiries"] as $key => $value) {
if (time() > $value) {
unset($_SESSION['cache'][$key]);
unset($_SESSION["expiries"][$key]);
}
}
}
}
@joeyboli
Copy link

thank you man...

@jaheyy
Copy link

jaheyy commented Jan 21, 2024

Thanks, works great. However, there are two caveats I found:

  • $requests are iterated from 0, so if I set allowedRequests to 1, it actually allows 2 requests. It can be fixed by setting $requests initial value to 1
  • Client can quite easily hack this rate limitter by simply deleting or changing session ID cookie. To make it safer, we can create session_id based on caller IP address + ideally salt. Example implementation below:
<?php

class IpBasedSessionStarter {
    private string $salt;

    public function __construct(string $salt) {
        $this->salt = $salt;
    }
    
    public function start() {
        $ip = $_SERVER['REMOTE_ADDR'];
        session_id(md5($this->salt . $ip));
        session_start();
    }
}

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