Hi Guys,

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

  1. 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.
  2. waitForRequest
    • Continuously checks if a request is allowed within the timeoutDuration.
    • Prevents excessive CPU usage by sleeping for 10ms between checks.

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

  1. Immediate Requests
    Requests are immediately allowed if the limit hasn’t been reached.
  2. Rate Limit Exceeded
    If the rate limit is exceeded, the script waits for a slot to become available, within the timeoutDuration.
  3. 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!

Loading