symfony/rate-limiter
Symfony Rate Limiter component implementing token bucket rate limiting. Configure limiters via a factory and use reserve() to wait for tokens or consume() to attempt immediately. Supports pluggable storage like in-memory for controlling request/input/output rates.
Installation:
composer require symfony/rate-limiter
Basic Usage:
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\RateLimiter\Storage\InMemoryStorage;
// Initialize factory with a unique ID and rate policy
$factory = new RateLimiterFactory([
'id' => 'login_attempts',
'policy' => 'token_bucket',
'limit' => 5, // Max tokens
'rate' => ['interval' => '5 minutes'], // Refill rate
], new InMemoryStorage());
$limiter = $factory->create();
First Use Case:
$limiter->reserve(1)->wait();
// Execute protected logic here
if ($limiter->consume(1)->isAccepted()) {
// Execute logic if tokens are available
}
RateLimiterFactory, TokenBucketRateLimiter, and Storage interfaces.use Symfony\Component\RateLimiter\RateLimiterInterface;
class RateLimitMiddleware
{
public function __construct(private RateLimiterInterface $limiter) {}
public function handle($request, Closure $next)
{
if (!$this->limiter->consume(1)->isAccepted()) {
return response()->json(['error' => 'Rate limit exceeded'], 429)
->header('Retry-After', $this->limiter->getRetryAfter()->format('U'));
}
return $next($request);
}
}
$factory = new RateLimiterFactory([
'id' => 'api_endpoint',
'policy' => 'token_bucket',
'limit' => config('rate_limits.api.max_attempts'),
'rate' => ['interval' => config('rate_limits.api.window')],
], $storage);
use Symfony\Component\RateLimiter\RateLimiterInterface;
class ProcessPaymentJob implements ShouldQueue
{
public function __construct(private RateLimiterInterface $limiter) {}
public function handle()
{
$this->limiter->reserve(1)->wait();
// Process payment
}
}
use Symfony\Component\RateLimiter\CompoundRateLimiterFactory;
$ipLimiter = $factory->create('ip_limit');
$userLimiter = $factory->create('user_limit');
$compoundFactory = new CompoundRateLimiterFactory([$ipLimiter, $userLimiter]);
$compoundLimiter = $compoundFactory->create();
use Symfony\Component\RateLimiter\Storage\RedisStorage;
use Predis\Client;
$redis = new Client(['scheme' => 'tcp', 'host' => 'redis']);
$storage = new RedisStorage($redis);
Service Provider:
public function register()
{
$this->app->singleton(RateLimiterInterface::class, function ($app) {
$storage = new RedisStorage($app['redis']);
return (new RateLimiterFactory([
'id' => 'default',
'policy' => 'token_bucket',
'limit' => 100,
'rate' => ['interval' => '1 hour'],
], $storage))->create();
});
}
Facade:
// app/Facades/RateLimiter.php
public static function limit(string $id, int $tokens = 1): bool
{
return app(RateLimiterInterface::class)->consume($tokens)->isAccepted();
}
Mock Storage for Unit Tests:
$storage = $this->createMock(StorageInterface::class);
$storage->method('get')->willReturn(['tokens' => 10, 'lastConsumed' => now()]);
$factory = new RateLimiterFactory([...], $storage);
Behavioral Tests:
$limiter = $factory->create();
$this->assertTrue($limiter->consume(1)->isAccepted());
$this->assertFalse($limiter->consume(10)->isAccepted());
InMemoryStorage Limitations:
RedisStorage for distributed systems.Token Bucket Quirks:
token_bucket policy allows short bursts (up to limit tokens). For strict fixed windows, use fixed_window or sliding_window policies.isAccepted() or use reserve()->wait().Retry-After Headers:
retryAfter may not be perfectly accurate due to clock skew. Test with real-world traffic.Storage Backend Bottlenecks:
Log Rate Limit Events:
$limiter->consume(1)->isAccepted(); // Returns a Result object
if (!$result->isAccepted()) {
\Log::warning('Rate limit exceeded', [
'retry_after' => $result->getRetryAfter()->diffInSeconds(),
'remaining' => $result->getRemainingTokens(),
]);
}
Inspect Storage State:
$storage = $factory->getStorage();
$state = $storage->get('login_attempts'); // Debug raw storage
Common Issues:
DateTime::getLastErrors().Custom Storage:
class DatabaseStorage implements StorageInterface
{
public function get(string $id): array
{
return DB::table('rate_limits')->where('id', $id)->first();
}
public function set(string $id, array $state): void
{
DB::table('rate_limits')->updateOrInsert(
['id' => $id],
['state' => json_encode($state)]
);
}
}
Dynamic Policies:
$factory = new RateLimiterFactory([
'id' => 'dynamic_limit',
'policy' => function (StorageInterface $storage) {
$config = config('rate_limits.dynamic');
return new TokenBucketRateLimiter(
$storage,
$config['limit'],
new \DateInterval($config['interval'])
);
},
], $storage);
Event-Driven Rate Limiting:
$limiter->consume(1)->onExceeded(function (Result $result) {
event(new RateLimitExceeded($result->getRetryAfter()));
});
Queue Workers:
Artisan Commands:
$limiter
How can I help you explore Laravel packages today?