sanmai/rate-limiter

Cache-based rate limiter using sliding window algorithm

0.1.1 2025-07-25 08:32 UTC

This package is auto-updated.

Last update: 2025-07-25 10:02:06 UTC


README

Latest Stable Version

Cache-based API rate limiting for PHP applications.

Table of Contents

Installation

composer require sanmai/rate-limiter

Overview

Rate limiting with the sliding window approach.

Real-world example: Imagine you need to limit API requests to 100 per minute and 1000 per hour per client. This library lets you create a rate limiter with a 1-minute window and 1-hour observation period, then check if a client exceeds either of these limits.

Features

  • Two-level limiting - Window-based and period-based limits
  • Lazy evaluation - Calculates limits only when needed
  • PSR-compatible - Easily integrates with PSR-15 middleware

How it works (the simple version)

This rate limiter provides two types of limits:

  1. Window limits - Controls request rates in the most recent time window (e.g., 100 requests per minute)
  2. Period limits - Controls total requests over a longer observation period (e.g., 1000 requests per hour)

The rate limiter itself tracks requests, while the limits are set when checking if they've been exceeded.

Quick Start

Setting up a rate limiter

// Import necessary classes
use SlidingWindowCounter\RateLimiter\RateLimiter;
use SlidingWindowCounter\Cache\MemcachedAdapter;

// Create a rate limiter for an IP address with 1-minute windows and 1-hour observation period
$rateLimiter = RateLimiter::create(
    '192.168.1.1',          // Subject being rate limited (e.g., IP address)
    'api_requests',         // Name for your rate limiter
    60,                     // Window size: 60 seconds (1 minute)
    3600,                   // Observation period: 3600 seconds (1 hour)
    new MemcachedAdapter($memcached)
);

Tracking requests

// Record a request from this client
$rateLimiter->increment();

// You can also increment by a specific amount (for weighted actions)
$rateLimiter->increment(2); // Count this action as 2 requests

Checking rate limits

// Check if the client has exceeded window limit (100 requests per minute)
$windowResult = $rateLimiter->checkWindowLimit(100);

if ($windowResult->isLimitExceeded()) {
    // Window limit exceeded - client is sending requests too quickly
    echo $windowResult->getLimitExceededMessage();
    // Example output: "Rate limit exceeded for 192.168.1.1: 120 actions in the window (limit: 100)"
    
    // Return 429 Too Many Requests response
    header('HTTP/1.1 429 Too Many Requests');
    header('Retry-After: 60');
    exit;
}

// Check if the client has exceeded period limit (1000 requests per hour)
$periodResult = $rateLimiter->checkPeriodLimit(1000);

if ($periodResult->isLimitExceeded()) {
    // Period limit exceeded - client has sent too many requests in the observation period
    echo $periodResult->getLimitExceededMessage();
    
    // Return 429 Too Many Requests response
    header('HTTP/1.1 429 Too Many Requests');
    header('Retry-After: 3600');
    exit;
}

Getting more information

// Get information about the current rate limit status
$windowResult = $rateLimiter->checkWindowLimit(100);

// Subject being rate limited
$subject = $windowResult->getSubject(); // e.g., "192.168.1.1"

// Current count in the window
$count = $windowResult->getCount();

// Maximum limit
$limit = $windowResult->getLimit();

// Type of limit
$limitType = $windowResult->getLimitType(); // "window" or "period"

// Get the limit message (only if exceeded)
$message = $windowResult->getLimitExceededMessage();

// Get the latest value in the current window
$currentValue = $rateLimiter->getLatestValue();

// Get the total across all windows in the observation period
$totalRequests = $rateLimiter->getTotal();

Advanced Usage

Using multiple rate limiters for different constraints

You can create different rate limiters for different types of constraints:

// General rate limiter with 1-minute windows and 1-hour observation period
$generalLimiter = RateLimiter::create($clientIp, 'general_api', 60, 3600, $cache);

// Check if client exceeds 100 requests per minute
$windowResult = $generalLimiter->checkWindowLimit(100);

// Check if client exceeds 1000 requests per hour
$periodResult = $generalLimiter->checkPeriodLimit(1000);

// Stricter limiter for sensitive endpoints with same time parameters
$sensitiveLimiter = RateLimiter::create($clientIp, 'sensitive_api', 60, 3600, $cache);

// Check if client exceeds 10 requests per minute for sensitive endpoints
$sensitiveWindowResult = $sensitiveLimiter->checkWindowLimit(10);

// Check if client exceeds 50 requests per hour for sensitive endpoints
$sensitivePeriodResult = $sensitiveLimiter->checkPeriodLimit(50);

Implementing in middleware

Here's how you might implement rate limiting in a PSR-15 middleware:

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
    $ip = $request->getServerParams()['REMOTE_ADDR'];
    
    // Create rate limiter
    $rateLimiter = RateLimiter::create($ip, 'api_requests', 60, 3600, $this->cache);
    
    // Increment the counter
    $rateLimiter->increment();
    
    // Check window limit (e.g., 100 requests per minute)
    $windowResult = $rateLimiter->checkWindowLimit(100);
    if ($windowResult->isLimitExceeded()) {
        return $this->createRateLimitResponse(
            $windowResult->getLimitExceededMessage(),
            60
        );
    }
    
    // Check period limit (e.g., 1000 requests per hour)
    $periodResult = $rateLimiter->checkPeriodLimit(1000);
    if ($periodResult->isLimitExceeded()) {
        return $this->createRateLimitResponse(
            $periodResult->getLimitExceededMessage(),
            3600
        );
    }
    
    // Limits not exceeded, continue with the request
    return $handler->handle($request);
}

Error Handling

Here are some common scenarios and how to handle them:

try {
    // Create the rate limiter
    $rateLimiter = RateLimiter::create($ip, 'api_requests', 60, 3600, $cache);
    
    // Increment and check limits
    $rateLimiter->increment();
    $windowResult = $rateLimiter->checkWindowLimit(100);
    
    // Handle rate limit exceeded
    if ($windowResult->isLimitExceeded()) {
        // Log the rate limit event
        $this->logger->warning('Rate limit exceeded', [
            'ip' => $ip,
            'count' => $windowResult->getCount(),
            'limit' => $windowResult->getLimit(),
            'type' => $windowResult->getLimitType()
        ]);
        
        // Return appropriate response
        return $this->createRateLimitResponse(
            $windowResult->getLimitExceededMessage(),
            60
        );
    }
} catch (Exception $e) {
    // If the cache service is unavailable, fail open (allow the request)
    $this->logger->error('Rate limiter error', ['exception' => $e]);
    
    // Continue processing the request
    return $handler->handle($request);
}

Cache Adapters

This library uses the cache adapters provided by the sanmai/sliding-window-counter library. For information about available adapters and how to create your own, please refer to the sliding window counter documentation.

License

License

See the LICENSE file for details.