sanmai / rate-limiter
Cache-based rate limiter using sliding window algorithm
Requires
- php: ^8.2
- sanmai/later: ^0.1.5
- sanmai/pipeline: ^6.11 || ^7.0
- sanmai/sliding-window-counter: ^0.2.1
Requires (Dev)
- ergebnis/composer-normalize: ^2.8
- friendsofphp/php-cs-fixer: ^3.17
- infection/infection: >=0.10.5
- phan/phan: >=1.1
- phpstan/phpstan: >=0.10
- phpunit/phpunit: >=10 <12
- vimeo/psalm: >=2
This package is auto-updated.
Last update: 2025-07-25 10:02:06 UTC
README
Cache-based API rate limiting for PHP applications.
Table of Contents
- Overview
- Features
- How It Works
- Installation
- Quick Start
- Advanced Usage
- Cache Adapters
- Technical Details
- Contributing
- License
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:
- Window limits - Controls request rates in the most recent time window (e.g., 100 requests per minute)
- 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
See the LICENSE file for details.