avanboxel / php-rate-limiter
A flexible rate limiting library for PHP applications with multiple algorithm implementations and persistent storage support
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/avanboxel/php-rate-limiter
Requires
- php: >=8.0
- ext-json: *
- ext-pdo: *
- predis/predis: ^3.0
Requires (Dev)
- phpunit/phpunit: ^12.2
Suggests
- ext-mysql: For MySQL/MariaDB storage support
- ext-redis: For Redis storage with better performance
README
A flexible rate limiting library for PHP applications with multiple algorithm implementations and persistent storage support.
Installation
composer require avanboxel/php-rate-limiter
Rate Limiting Algorithms
This library provides four different rate limiting algorithms, each with unique characteristics:
1. Token Bucket
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";
2. Leaky Bucket
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";
3. Fixed Window
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";
4. Sliding Window
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 Comparison
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 |
Quick Start
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.
Basic Usage with RateLimiter
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"; }
Storage Backends
Redis Storage
use PhpRateLimiter\Storage\RedisRateLimitStorage; $redis = new \Predis\Client([ 'scheme' => 'tcp', 'host' => 'localhost', 'port' => 6379, ]); $storage = new RedisRateLimitStorage($redis);
MySQL Storage
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.sql
You 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
Creating Custom Storage Solutions
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';
}
}
Common Interface
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;
Advanced Usage
Custom Time Provider (for testing)
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);
Complete Example with Multiple Storage Backends
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 Limiting Different Resources (Direct Algorithm Usage)
// 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");
Examples
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