Luminova Framework

PHP Luminova: Request Rate Limiter

Last updated: 2025-04-11 08:06:30

Rate Limiter class provides a lightweight and flexible methods for controlling the frequency of requests made by clients to your application.

Rate Limiter is a lightweight and flexible class that provides methods for controlling the frequency of requests made by clients to your application. It is designed to prevent abuse or overuse of resources by enforcing rate limits based on a unique client key—such as a user ID, IP address, or identifier key.

This class supports a plug-and-play approach with various caching backends like FileCache, Redis, Memcached, CacheItemPoolInterface andCacheInterface. You can define how many requests are allowed (limit) within a given time window (ttl), and enforce these rules globally, per controller, or per individual route.

The RateLimiter tracks each request and stores usage data in the provided cache instance. When the request threshold is exceeded, it offers clean response handling and headers that inform clients of their remaining quota and retry wait time.


Key Features:

  • Works with any PSR-compatible cache or Luminova cache classes.
  • Prevents request flooding and abuse.
  • Supports dynamic limit policies across different scopes (global, controller, or route).
  • Easy to integrate into onPreCreate, middlewares, or specific methods.
  • Provides customizable response messages and HTTP headers.

Usage Examples

Supported Cache Backends

The RateLimiter supports multiple caching backends, allowing flexible implementation based on your environment or preferences.

Using Luminova's FileCache (default fallback):

use Luminova\Cache\FileCache;

$path = root('/writeable/caches/path/to/limiter');
$file = new FileCache($path);

Using Redis:

use Redis;

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

Using Memcached:

use Memcached;

$memcached = new Memcached();
$memcached->addServer('127.0.0.1', 11211);

Basic Rate Limiting

use Luminova\Security\RateLimiter;
use Memcached;

$key = 'user-id';

$cache = new Memcached();
$cache->addServer('127.0.0.1', 11211);

$limiter = new RateLimiter(cache: $cache, limit: 10, ttl: 60);

if (!$limiter->check($key)->isAllowed()) {
    $limiter->message('Rate limit exceeded: {limit} requests. Retry after {retry}. Remaining: {remaining}.')
         ->respond();

    // Or throw an exception
    // throw new TooManyRequestException('Too many requests');
}

Updating request limit with new limit.

if (!$limiter->check($key)->isAllowed()) {
    // Just an example
    if($limiter->isIpAddress()){
        $limiter->continue(2);
    }else{
        $limiter->message('Rate limit exceeded: {limit} requests. Retry after {retry}. Remaining: {remaining}.')
            ->respond();
    }
}

Synchronous Rate Limiting:

Perform a rate‑limit check and block execution until the check completes.

use Luminova\Security\RateLimiter;
use Memcached;

$key = 'user-id';

$cache = new Memcached();
$cache->addServer('127.0.0.1', 11211);

$limiter = new RateLimiter(cache: $cache, limit: 10, ttl: 60);

// Check and wait until the rate‑limit check resolves
$limiter->check($key)->wait();

if (!$limiter->isAllowed()) {
    $limiter->message('Rate limit exceeded: {limit} requests. Retry after {retry}. Remaining: {remaining}.')
         ->respond();
}

Advanced Usage

Application Scope Rate Limiting

You can enforce global rate limiting by placing the logic inside the onPreCreate() method of your application class. This ensures rate limits are checked at the earliest stage—before any routing or service initialization occurs—allowing the application to terminate immediately if the request exceeds the allowed threshold. This approach is ideal for protecting all incoming API traffic with a consistent policy.

// /app/Application.php
namespace App;

use Luminova\Core\CoreApplication;
use Luminova\Security\RateLimiter;
use Memcached;

class Application extends CoreApplication 
{
    protected function onPreCreate(): void 
    {
        $key = request()->header->get('X-Api-User-Id');

        $cache = new Memcached();
        $cache->addServer('127.0.0.1', 11211);

        $limiter = new RateLimiter(cache: $cache, limit: 10, ttl: 60);

        if (!$limiter->check($key)->isAllowed()) {
            $limiter->message('Rate limit exceeded.')
                 ->respond();

            $this->terminate(true, [
                'userId' => $key
            ]);
        }
    }

    protected function onTerminated(array $info): void 
    {
        logger('debug', 'Application was terminated', $info);
    }
}

Controller Scope Rate Limiting

Apply rate limiting to specific controllers or routes when different policies are needed per endpoint, or to exclude certain routes entirely.

In onCreate() Method (Controller-wide)

// /app/Controllers/Http/ApiController.php
namespace App\Controllers\Http;

use Luminova\Bae\BaseController;
use Luminova\Security\RateLimiter;
use Memcached;

class ApiController extends BaseController 
{
    protected function onCreate(): void 
    {
        $key = request()->header->get('X-Api-User-Id');

        $cache = new Memcached();
        $cache->addServer('127.0.0.1', 11211);

        $limiter = new RateLimiter(cache: $cache, limit: 10, ttl: 60);

        if (!$limiter->check($key)->isAllowed()) {
            $limiter->message('Rate limit exceeded.')
                 ->respond();
            exit;
        }
    }
}

As Middleware (Before Route Execution)

Apply rate limiting within a controller's before middleware to enforce limits before any route-specific logic is executed. This ensures that all requests to the controller or a specific URL prefix attached to the controller class are evaluated early—before any view or template rendering occurs.

// /app/Controllers/Http/ApiController.php
namespace App\Controllers\Http;

use Luminova\Bae\BaseController;
use Luminova\Security\RateLimiter;
use Luminova\Attributes\Route;
use Luminova\Attributes\Prefix;
use Luminova\Http\Request;

#[Prefix(pattern: '/api(:root)')]
class ApiController extends BaseController 
{
    #[Route('/api/(:root)', methods: ['ANY'], middleware: Route::BEFORE_MIDDLEWARE)]
    public function middleware(Request $request): int
    {
        $key = $request->header->get('X-Api-User-Id');

        return $this->rate->check($key)->isAllowed()
            ? STATUS_SUCCESS
            : STATUS_ERROR;
    }

    protected function onMiddlewareFailure(string $uri, array $classInfo): void 
    {
        $key = request()->header->get('X-Api-User-Id');

        response(429, $this->rate->getHeaders())->json([
            'error' => 'Rate limit exceeded'
        ]);
    }
}

Per-Route Rate Limiting

Apply rate limiting directly within a specific controller method to enforce limits on a per-endpoint basis. This allows requests to be evaluated just before the view logic is executed, ensuring only permitted clients can access the response.

// /app/Controllers/Http/ApiController.php
namespace App\Controllers\Http;

use Luminova\Bae\BaseController;
use Luminova\Security\RateLimiter;
use Luminova\Attributes\Route;
use Luminova\Http\Request;
use App\Models\Users;

class ApiController extends BaseController 
{
    #[Route('/api/users/', methods: ['GET'])]
    public function users(Request $request): int 
    {
        $key = $request->header->get('X-Api-User-Id');

        $cache = new Memcached();
        $cache->addServer('127.0.0.1', 11211);

        $limiter = new RateLimiter(cache: $cache, limit: 10, ttl: 60);

        if ($limiter->check($key)->isAllowed()) {
            return response()->json([
                'data' => (new Users())->select()
            ]);
        }

        return response(429, $limiter->getHeaders())->json([
            'error' => 'Rate limit exceeded'
        ]);
    }
}

Class Definition


Methods

constructor

Create a new RateLimiter instance.

This constructor allows flexible integration with different caching backends to track and limit request rates per client (typically IP-based).

Supported Cache Instance

  • PSR-6: CacheItemPoolInterface (e.g., CustomCache)
  • PSR-16: CacheInterface (e.g., Psr\SimpleCache\CacheInterface)
  • BaseCache: Luminova's custom file or memory cache.
  • Memcached: Native Memcached instance.
  • Redis: PHP Redis extension instance.
  • PredisClient: Predis library instance.
public __construct(
    Psr\Cache\CacheItemPoolInterface|Psr\SimpleCache\CacheInterface|Luminova\Base\BaseCache|Memcached|Predis\Client|Redis|null $cache = null, 
    int $limit = 10, 
    DateInterval|int $ttl = 60
    string $persistentId = ''
): mixed

Parameters:

ParameterTypeDescription
$cachemixedOptional cache instance. If null, the default Luminova file-based cache will be used.
$limitintMaximum number of allowed requests within the TTL window (default: 10).
$ttlDateInterval|intTime-to-live for request tracking, in seconds or as a DateInterval (default: 60).
$persistentIdstringOptional unique ID to be included with key.

setCache

Set the cache instance used for rate limiting.

This method allows you to provide a custom caching backend to storerate-limiting data. Supported cache types include:

  • PSR-6 (CacheItemPoolInterface) and PSR-16 (CacheInterface) implementations
  • Native Memcached or Redis instances
  • PredisClient instance
  • Luminova's custom BaseCache

If no cache is set explicitly, the default fallback is Luminova's file-based cache.

public setCache(
    Psr\Cache\CacheItemPoolInterface|Psr\SimpleCache\CacheInterface|Luminova\Base\BaseCache|Memcached|Predis\Client|Redis $cache
): self

Parameters:

ParameterTypeDescription
$cachemixedThe cache instance used to store limiter data.

Return Value:

self - Returns the instance of RateLimiter class.


getHeaders

Get the proper headers for rate limitation.

public getHeaders(): array<string,mixed>

Return Value:

array<string,mixed> - Return an associative array of headers.


getExtendedLimit

Get the total number of extended requests limit added via (continue).

public getExtendedLimit(): int

Return Value:

int - Return the extended request limit or 0 if none.


getLimit

Get the maximum number of allowed requests within the time window.

public getLimit(): int

Return Value:

int - Return the configured request limit.


getRequests

Get the current number of request attempts made during the current time window.

public getRequests(): int

Return Value:

int - Return the number of requests made so far.


getRemaining

Get the number of remaining allowed requests in the current time window.

public getRemaining(): int

Return Value:

int - Return the remaining number of requests before limit is reached.


getRetryAfter

Get the number of seconds until the rate limit resets.

public getRetryAfter(): int

Return Value:

int - Return the seconds until the next request will be allowed.


getReset

Get the exact UNIX timestamp when the rate limit will reset.

public getReset(): int

Return Value:

int - Return the timestamp of rate limit reset.


getTimeWindow

Get the time window in seconds used for rate limiting.

public getTimeWindow(): int

Return Value:

int - Return the duration of the rate-limiting window in seconds.


getIp

Retrieves the stored client IP address associated with the current request context.

public getIp(): ?string

Return Value:

string|null - Return the IP address if available, or null if not set.


isAllowed

Retrieves the result after performing the check method.

public isAllowed(): bool

Return Value:

bool - Returns true if the request is allowed, false if the rate limit has been reached.


isIpAddress

Determines whether the current request is from the same IP address that initialized the key.

This method returns true if this is the first request, or if the stored IP address matches the current IP.

public isIpAddress(): bool

Return Value:

bool - Return true if the IP matches or it's the first request; otherwise, false.

Useful for validating the continuity of a rate-limited session or key-bound identity.


check

Checks if a request is allowed based on the rate limit for the given key.

This method generates a unique key based on the clients's IP address or default to provided key.It uses MD5 hashing for simplicity in key hashing (e.g., Rate-Limiter:{Persistent-Id}+{Custom-Key||Ip-Address}).

public check(?string $key = null): self

Parameters:

ParameterTypeDescription
$keystring|nullAn optional custom identifier to check the rate limit for (e.g., User-Id).

Return Value:

self - Returns the instance of the RateLimiter class, reflecting the status.

Throws:


wait

Wait until the rate limiter is marked as finished or max wait time is exceeded.

public wait(float|int $interval = 1.0, ?int $maxWait = null): void

Parameters:

ParameterTypeDescription
$intervalfloat|intInterval in seconds (supports fractions like 0.1 for 100ms).
$maxWaitint|nullMaximum seconds to wait. Null for infinite wait.

remaining

Generate the number of remaining allowed requests for the given key.

public remaining(?string $key = null): int

Parameters:

ParameterTypeDescription
$keystring|nullAn optional unique identifier to check (e.g, User-Id).
Default to client IP address if key is not provided.

Return Value:

int - Return the remaining number of requests before the limit is reached.


continue

Resets the rate-limiting state to allow additional requests to continue processing.

This method updates the internal request counters by subtracting the given $limit from the total consumed request count and adding it back to the remaining allowance. It also resets the finished and exceeded flags to indicate that the current rate-limiting session is active and within bounds.

public continue(int $limit = 1): self

Parameters:

ParameterTypeDescription
$limitintThe number of requests to re-allow or reassign (default is 1).

Return Value:

self - Returns the instance of the RateLimiter class, reflecting the status.

Throws:


reset

Reset remaining rate to initial limit and consumed request to 0.

public reset(?string $key = null): self

Parameters:

ParameterTypeDescription
$keystring|nullAn optional unique identifier to rest requests (e.g, User-Id).
Default to client IP address if key is not provided.

Return Value:

self - Returns the instance of the RateLimiter class.

Throws:


has

Check whether the rate limiter has an entry for the given key.

public has(?string $key = null): bool

Parameters:

ParameterTypeDescription
$keystring|nullAn optional unique identifier to check (e.g, User-Id).
Default to client IP address if key is not provided.

Return Value:

bool - Return true if the key exists in the cache, false otherwise.

Throws:


message

Sets the response message and its output format.

This method allows you to define the body of the rate limit response in various formats.If the message is a string, you can include the following placeholders which will be replaced automatically:

Placeholder Variables:

  • {limit} - Total number of allowed requests.
  • {remaining} - Number of requests remaining.
  • {reset} - Timestamp when the limit resets.
  • {retry} - Seconds to wait before retrying.

Supported Output Formats:

  • json - Outputs as JSON. Arrays/objects are encoded automatically.
  • html - Outputs as HTML string.
  • xml - Outputs as XML string.
  • text - Outputs as plain text.
  • custom - Outputs with custom format; you must manually set the Content-Type header.
public message(string|array|object $body, string $type = 'json'): self

Parameters:

ParameterTypeDescription
$bodystring|array|objectThe response body or message to output.
$typestringThe response message format (e.g, html).

Return Value:

self - Return instance of RateLimiter class.

Examples:

// As JSON with message placeholders
$rateLimiter->message("You have {remaining} of {limit} requests left. Try again in {retry} seconds.");

// As plain text
$rateLimiter->message("Rate limit exceeded. Please retry later.", 'text');

// With structured array (auto-converted if not JSON format)
$rateLimiter->message([
    'error' => 'Rate limit exceeded',
    'retry_after' => 60
], 'html');

respond

Send an HTTP 429 (Too Many Requests) response with appropriate rate-limit headers.

public respond(array<string,string> $headers = []): int

Parameters:

ParameterTypeDescription
$headersarray<string,string>Additional headers to include in the response key-pare.

Return Value:

int - Return response status (e.g, STATUS_*).