Rate limiting is a crucial aspect of web applications, especially when handling APIs or user requests. It helps prevent abuse, ensures fair usage, and protects your server from being overwhelmed. In this blog post, we’ll explore how to implement a lightweight rate limiter in PHP using the provided code example.
What Is Rate Limiting?
Rate limiting controls the number of requests a client can make within a specified time frame. If a client exceeds this limit, the rate limiter either denies further requests or makes the client wait until the limit resets. This technique is commonly used to:
- Prevent API abuse.
- Optimize server performance.
- Maintain fair resource usage.
The PHP Rate Limiter Implementation
Below is a breakdown of the RateLimiter
class and its accompanying configuration and usage files.
RateLimiter Class
The RateLimiter
class is the core of the implementation. It tracks incoming requests, decides whether to allow or deny them, and provides an optional mechanism to wait until the request is permitted.
Here’s the complete code for the RateLimiter
class:
<?php class RateLimiter { private int $limitForPeriod; private float $limitRefreshPeriod; // in seconds private float $timeoutDuration; // in seconds private array $requests = []; // Stores timestamps of requests private $semId; // Semaphore identifier /** * Constructor for RateLimiter. * * @param int $limitForPeriod Maximum number of requests allowed in the period. * @param float $limitRefreshPeriod Time period in seconds to refresh the limit. * @param float $timeoutDuration Maximum time in seconds to wait for a request to be allowed. */ public function __construct(int $limitForPeriod, float $limitRefreshPeriod, float $timeoutDuration) { $this->limitForPeriod = $limitForPeriod; $this->limitRefreshPeriod = $limitRefreshPeriod; $this->timeoutDuration = $timeoutDuration; // Initialize a semaphore $this->semId = sem_get(ftok(__FILE__, 'R')); if ($this->semId === false) { throw new RuntimeException('Unable to create semaphore'); } } /** * Acquire the semaphore lock. */ private function lock() { if (!sem_acquire($this->semId)) { throw new RuntimeException('Unable to acquire semaphore lock'); } } /** * Release the semaphore lock. */ private function unlock() { if (!sem_release($this->semId)) { throw new RuntimeException('Unable to release semaphore lock'); } } /** * Checks if a request can proceed within the rate limit. * * @return bool True if allowed, False otherwise. */ public function allowRequest(): bool { $this->lock(); $now = microtime(true); // Remove requests outside the current refresh window $this->requests = array_filter($this->requests, function ($timestamp) use ($now) { return ($now - $timestamp) <= $this->limitRefreshPeriod; }); // Check if request can proceed if (count($this->requests) < $this->limitForPeriod) { $this->requests[] = $now; // Add the new request timestamp $this->unlock(); return true; } $this->unlock(); return false; } /** * Waits until a request is allowed or the timeout expires. * * @return bool True if allowed after waiting, False if timeout. */ public function waitForRequest(): bool { $start = microtime(true); while ((microtime(true) - $start) <= $this->timeoutDuration) { if ($this->allowRequest()) { return true; } usleep(10000); // Sleep for 10ms to prevent busy-waiting } return false; } } ?>
Key Methods
allowRequest
- Filters out expired requests from the tracking array based on the
limitRefreshPeriod
. - Allows the request if the count of valid requests is within the specified limit.
- Filters out expired requests from the tracking array based on the
waitForRequest
- Continuously checks if a request is allowed within the
timeoutDuration
. - Prevents excessive CPU usage by sleeping for 10ms between checks.
- Continuously checks if a request is allowed within the
Configuration: rate_limiter_config.php
This file sets up the RateLimiter
with custom parameters:
limitForPeriod
: Maximum number of requests allowed.limitRefreshPeriod
: Time frame in seconds after which the limit resets.timeoutDuration
: Maximum time to wait for a request to be allowed.
require_once 'RateLimiter.php'; // Create a RateLimiter instance with custom parameters $rateLimiter = new RateLimiter( limitForPeriod: 2, // Max 2 requests limitRefreshPeriod: 1.0, // Reset limit every 1 second timeoutDuration: 0.1 // Timeout after 100 milliseconds ); return $rateLimiter;
Example Usage: example.php
In this example, we simulate 10 requests with a 200ms interval between them. The script uses allowRequest
to decide if a request can proceed immediately. If the limit is exceeded, it calls waitForRequest
to see if the request can proceed after waiting.
require_once 'rate_limiter_config.php'; /** @var RateLimiter $rateLimiter */ $rateLimiter = require 'rate_limiter_config.php'; // Simulate requests for ($i = 0; $i < 10; $i++) { if ($rateLimiter->allowRequest()) { echo "Request $i: Allowed\n"; } else { echo "Request $i: Rate limit exceeded\n"; if ($rateLimiter->waitForRequest()) { echo "Request $i: Allowed after waiting\n"; } else { echo "Request $i: Denied after timeout\n"; } } usleep(200000); // Simulate delay between requests (200ms) }
How It Works
- Immediate Requests
Requests are immediately allowed if the limit hasn’t been reached. - Rate Limit Exceeded
If the rate limit is exceeded, the script waits for a slot to become available, within thetimeoutDuration
. - Timeout
If no slot becomes available before the timeout, the request is denied.
Example Output
When running the example.php
script, you might see the following output:
Request 0: Allowed Request 1: Allowed Request 2: Rate limit exceeded Request 2: Allowed after waiting Request 3: Allowed Request 4: Allowed Request 5: Rate limit exceeded Request 5: Denied after timeout ...
Practical Applications
This rate limiter can be used for:
- API Gateways: Enforcing request quotas for clients.
- Login Systems: Preventing brute-force attacks by limiting login attempts.
- Resource Throttling: Managing access to shared resources.
In conclusion, the RateLimiter
class provides a straightforward, flexible way to implement rate limiting in PHP. By customizing parameters such as limitForPeriod
, limitRefreshPeriod
, and timeoutDuration
, you can tailor it to various use cases. Whether you’re protecting an API or preventing misuse of a service, this implementation is a great starting point.
That’s it.
Hope you liked it.
Critics/feedbacks are welcome.
Have a great day ahead!
Leave a Reply