A flexible rate limiting library for PHP applications with multiple algorithm implementations and persistent storage support.
composer require avanboxel/php-rate-limiterThis library provides four different rate limiting algorithms, each with unique characteristics:
Best for: Allowing burst traffic while maintaining average rate
The token bucket starts full and refills at a constant rate. Requests consume tokens; when tokens are depleted, requests are rejected.
use PhpRateLimiter\Algorithm\TokenBucket; // Allow 10 requests max, refill 5 tokens per second $rateLimiter = new TokenBucket( capacity: 10, // Maximum tokens in bucket refillRate: 5, // Tokens added per refill period refillPeriod: 1 // Refill period in seconds (default: 1) ); if ($rateLimiter->attempt('user-123')) { echo "Request allowed\n"; } else { echo "Rate limited - try again at: " . date('Y-m-d H:i:s', $rateLimiter->availableAt('user-123')) . "\n"; } // Check remaining capacity echo "Retries left: " . $rateLimiter->retriesLeft('user-123') . "\n";Best for: Smoothing irregular traffic into steady output rate
The leaky bucket queues requests and processes them at a fixed rate. Excess requests overflow and are rejected.
use PhpRateLimiter\Algorithm\LeakyBucket; // Queue up to 5 requests, process 2 requests per second $rateLimiter = new LeakyBucket( capacity: 5, // Maximum requests in queue leakRate: 2, // Requests processed per leak period leakPeriod: 1 // Leak period in seconds (default: 1) ); if ($rateLimiter->attempt('api-client-456')) { echo "Request queued for processing\n"; } else { echo "Queue full - request rejected\n"; } // Check queue space echo "Queue slots available: " . $rateLimiter->retriesLeft('api-client-456') . "\n";Best for: Simple implementation with predictable windows
Divides time into fixed windows and counts requests within each window. Simple but can allow bursts at window boundaries.
use PhpRateLimiter\Algorithm\FixedWindow; // Allow 100 requests per 5-minute window $rateLimiter = new FixedWindow( maxRequests: 100, // Maximum requests per window windowSizeSeconds: 300 // Window size in seconds (5 minutes) ); if ($rateLimiter->attempt('endpoint-789')) { echo "Request allowed\n"; } else { $nextWindow = $rateLimiter->availableAt('endpoint-789'); echo "Rate limited until: " . date('Y-m-d H:i:s', $nextWindow) . "\n"; } // Check window capacity echo "Requests left in current window: " . $rateLimiter->retriesLeft('endpoint-789') . "\n";Best for: Most accurate rate limiting with precise time tracking
Maintains exact timestamps of requests and slides the window continuously. Provides the most accurate rate limiting.
use PhpRateLimiter\Algorithm\SlidingWindow; // Allow 50 requests per 60-second sliding window $rateLimiter = new SlidingWindow( maxRequests: 50, // Maximum requests in sliding window windowSizeSeconds: 60 // Window size in seconds ); if ($rateLimiter->attempt('premium-user-999')) { echo "Request allowed\n"; } else { echo "Rate limited - oldest request expires at: " . date('Y-m-d H:i:s', $rateLimiter->availableAt('premium-user-999')) . "\n"; } // Check sliding window capacity echo "Requests available: " . $rateLimiter->retriesLeft('premium-user-999') . "\n";| Algorithm | Memory Usage | Precision | Burst Handling | Use Case |
|---|---|---|---|---|
| Token Bucket | Low | Good | Excellent | APIs allowing bursts |
| Leaky Bucket | Low | Good | Smoothing | Traffic shaping |
| Fixed Window | Very Low | Fair | Poor | Simple rate limits |
| Sliding Window | High | Excellent | Good | Precise rate limiting |
The RateLimiter class is the main entry point that provides persistence for your rate limiting algorithms. It works with storage backends to maintain rate limit state across requests.
use PhpRateLimiter\RateLimiter; use PhpRateLimiter\Storage\RedisRateLimitStorage; use PhpRateLimiter\Algorithm\TokenBucket; // Set up storage (Redis in this example) $redis = new \Predis\Client(); $storage = new RedisRateLimitStorage($redis); // Create the main RateLimiter instance $rateLimiter = new RateLimiter($storage); // Use rate limiting $key = 'user:' . $userId; // Get existing algorithm or create new one if empty $algorithm = $rateLimiter->get($key); if (empty($algorithm)) { $algorithm = new TokenBucket(capacity: 50, refillRate: 5, refillPeriod: 60); } if ($algorithm->attempt($key)) { echo "Request allowed\n"; // Persist the algorithm state $rateLimiter->persist($algorithm, $key); } else { echo "Rate limited\n"; }use PhpRateLimiter\Storage\RedisRateLimitStorage; $redis = new \Predis\Client([ 'scheme' => 'tcp', 'host' => 'localhost', 'port' => 6379, ]); $storage = new RedisRateLimitStorage($redis);MySQL/MariaDB storage implementation with TTL support:
Database Setup:
Before using MySQL storage, create the required table by running the default.sql file:
mysql -u username -p database_name < default.sqlYou can also create a table with a different name by modifying the SQL statement and passing the custom table name to the constructor.
use PhpRateLimiter\Storage\MySqlRateLimitStorage; $pdo = new \PDO('mysql:host=localhost;dbname=myapp', $username, $password, [ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, ]); // Uses default table name 'rate_limit_storage' $storage = new MySqlRateLimitStorage($pdo); // Or specify custom table name $storage = new MySqlRateLimitStorage($pdo, 'custom_rate_limits');Features:
- JSON column for flexible state storage
- TTL support with automatic cleanup
- Proper indexing for performance
- UTF8MB4 charset for full Unicode support
You can create your own storage backend by implementing the RateLimitStorageInterface. This allows you to use any storage system (database, file system, in-memory cache, etc.) to persist rate limit state.
**Required Interface Methods:** - `saveState(string $key, array $state, ?int $ttl = null): void` - Store rate limit state with optional TTL - `getState(string $key): array` - Retrieve state array (return empty array if not found) - `deleteState(string $key): void` - Remove stored state for a key - `hasState(string $key): bool` - Check if state exists for a key **Implementation Guidelines:** 1. **Data Format**: State is always stored as an associative array. Serialize to JSON or your preferred format. 2. **TTL Support**: Handle optional time-to-live for automatic cleanup of expired data. 3. **Error Handling**: Throw appropriate exceptions for storage failures (`\RuntimeException`, `\JsonException`). 4. **Performance**: Consider indexing on the key column for database implementations. 5. **Key Handling**: Work with keys exactly as provided - no internal prefixing or modification. 6. **Cleanup**: Implement mechanisms to clean up expired entries if using TTL. **Simple Custom Storage Example:** ```php use PhpRateLimiter\Storage\RateLimitStorageInterface; class FileRateLimitStorage implements RateLimitStorageInterface { public function __construct(private string $storagePath) {} public function saveState(string $key, array $state, ?int $ttl = null): void { $data = [ 'state' => $state, 'expires_at' => $ttl ? time() + $ttl : null ]; $filename = $this->getFilename($key); file_put_contents($filename, json_encode($data, JSON_THROW_ON_ERROR), LOCK_EX); } public function getState(string $key): array { $filename = $this->getFilename($key); if (!file_exists($filename)) { return []; } $data = json_decode(file_get_contents($filename), true, 512, JSON_THROW_ON_ERROR); if ($data['expires_at'] && $data['expires_at'] <= time()) { unlink($filename); return []; } return $data['state'] ?? []; } public function deleteState(string $key): void { $filename = $this->getFilename($key); if (file_exists($filename)) { unlink($filename); } } public function hasState(string $key): bool { return !empty($this->getState($key)); } private function getFilename(string $key): string { return $this->storagePath . '/' . hash('sha256', $key) . '.json'; } } All rate limiters implement the same RateLimitAlgorithmInterface:
// Check if request is allowed $allowed = $rateLimiter->attempt(string $key): bool; // Check if rate limited (without consuming) $isLimited = $rateLimiter->isTooManyAttempts(string $key): bool; // Get remaining capacity $remaining = $rateLimiter->retriesLeft(string $key): int; // Get next available time $nextTime = $rateLimiter->availableAt(string $key): int; // Clear rate limit for key $rateLimiter->clear(string $key): void;use PhpRateLimiter\Algorithm\TokenBucket; // Create a custom time keeper for testing class MockTimeKeeper implements TimeKeeperInterface { public function __construct(private float $currentTime) {} public function getCurrentUnixMicroTimestamp(): float { return $this->currentTime; } public function advance(float $seconds): void { $this->currentTime += $seconds; } } $timeKeeper = new MockTimeKeeper(microtime(true)); $rateLimiter = new TokenBucket(10, 1, 1, $timeKeeper);use PhpRateLimiter\RateLimiter; use PhpRateLimiter\Storage\RedisRateLimitStorage; use PhpRateLimiter\Storage\MySqlRateLimitStorage; use PhpRateLimiter\Algorithm\TokenBucket; use PhpRateLimiter\Algorithm\SlidingWindow; // Redis storage for high-frequency rate limits $redis = new \Predis\Client(); $redisStorage = new RedisRateLimitStorage($redis); $redisRateLimiter = new RateLimiter($redisStorage); // MySQL storage for persistent rate limits $pdo = new \PDO('mysql:host=localhost;dbname=myapp', $username, $password); $mysqlStorage = new MySqlRateLimitStorage($pdo); $mysqlRateLimiter = new RateLimiter($mysqlStorage); // Rate limit by user (using Redis for speed) $userKey = "user:{$userId}"; $userAlgorithm = $redisRateLimiter->get($userKey) ?? new TokenBucket(100, 10); if ($userAlgorithm->attempt($userKey)) { echo "User request allowed\n"; $redisRateLimiter->persist($userAlgorithm, $userKey); } else { echo "User rate limited\n"; } // Rate limit by IP (using MySQL for persistence) $ipKey = "ip:{$clientIP}"; $ipAlgorithm = $mysqlRateLimiter->get($ipKey) ?? new SlidingWindow(1000, 3600); if ($ipAlgorithm->attempt($ipKey)) { echo "IP request allowed\n"; $mysqlRateLimiter->persist($ipAlgorithm, $ipKey); } else { echo "IP rate limited\n"; }// Rate limit by user $userLimiter = new TokenBucket(100, 10); // 100 req burst, 10/sec refill $userLimiter->attempt("user:{$userId}"); // Rate limit by IP $ipLimiter = new SlidingWindow(1000, 3600); // 1000 req/hour $ipLimiter->attempt("ip:{$clientIP}"); // Rate limit by API endpoint $endpointLimiter = new FixedWindow(500, 60); // 500 req/minute $endpointLimiter->attempt("endpoint:/api/heavy-operation");The examples/ directory contains practical demonstrations of how to use this library:
ApiExample.php- Demonstrates API rate limiting using Token Bucket algorithm to limit requests per API key (10 requests per minute)CmsExample.php- Shows CMS edit rate limiting with Fixed Window algorithm, allowing users 10 page edits per hour with status checkingSimpleMiddleware.php- Complete middleware implementation for web applications with HTTP headers and proper 429 responses using Token Bucket