Skip to content

sanmai/rate-limiter

Repository files navigation

Rate Limiter

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.

About

Cache-based API rate limiting for PHP apps

Resources

License

Stars

Watchers

Forks