symfony/rate-limiter
Symfony Rate Limiter provides token bucket rate limiting for your app. Create limiters with RateLimiterFactory and a storage backend (e.g., in-memory), then reserve tokens with blocking waits or consume instantly to allow/skip work based on availability.
## Getting Started
### Minimal Setup in Laravel (Updated for v8.1.0-BETA3)
1. **Install the package** (unchanged):
```bash
composer require symfony/rate-limiter
Configure a basic rate limiter (updated for v8.1.0-BETA3):
// config/rate_limits.php
return [
'login_attempts' => [
'policy' => 'token_bucket',
'limit' => 5,
'rate' => ['interval' => '5 minutes'],
'reservation_ttl' => '1 hour',
'window_mode' => 'calendar_aligned', // New: Calendar-aligned fixed window mode
],
'api_calls' => [
'policy' => 'sliding_window',
'limit' => 100,
'rate' => ['interval' => '1 minute'],
'grace_period' => 5,
'window_mode' => 'calendar_aligned', // New: Calendar-aligned fixed window mode
],
];
Service Provider Update (now leverages v8.1.0-BETA3's hardened window mode):
// app/Providers/RateLimiterServiceProvider.php
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\RateLimiter\Storage\RedisStorage;
use Symfony\Component\RateLimiter\Policy\SlidingWindowPolicy;
use Symfony\Component\RateLimiter\Policy\FixedWindowPolicy;
class RateLimiterServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton('rate_limiter.storage', function () {
return new RedisStorage($this->app['redis']);
});
$this->app->bind('rate_limiter.factory', function ($app) {
return new RateLimiterFactory(
config('rate_limits'),
$app['rate_limiter.storage'],
$app['config']['rate_limits.default_policy'] ?? 'token_bucket'
);
});
}
public function boot()
{
// Register new policy types with hardened window mode
RateLimiterFactory::addPolicy('sliding_window', function ($config) {
return new SlidingWindowPolicy(
$config['limit'],
$config['rate']['interval'],
$config['grace_period'] ?? 0
);
});
RateLimiterFactory::addPolicy('fixed_window', function ($config) {
return new FixedWindowPolicy(
$config['limit'],
$config['rate']['interval'],
$config['window_mode'] ?? 'fixed' // Default to 'fixed' for backward compatibility
);
});
}
}
Critical Update for Window Mode Handling (v8.1.0-BETA3 improvements):
// Updated middleware with calendar-aligned window mode support
public function handle($request, Closure $next)
{
$limiter = app('rate_limiter.factory')->create([
'id' => 'login_' . $request->ip(),
'policy' => config('rate_limits.login_attempts.policy'),
'limit' => config('rate_limits.login_attempts.limit'),
'rate' => config('rate_limits.login_attempts.rate'),
'reservation_ttl' => config('rate_limits.login_attempts.reservation_ttl'),
'window_mode' => config('rate_limits.login_attempts.window_mode') ?? 'fixed',
]);
$consumption = $limiter->consume(1);
if (!$consumption->isAccepted()) {
return response()->json(['error' => 'Too many attempts'], 429)
->header('Retry-After', $consumption->getRetryAfter()->format('U'))
->header('X-RateLimit-Reservations', $limiter->getPendingReservations());
}
return $next($request);
}
// Example: Payment endpoint with calendar-aligned window mode
public function processPayment(Request $request)
{
$limiter = app('rate_limiter.factory')->create([
'id' => 'payment_' . auth()->id(),
'policy' => 'fixed_window',
'limit' => 3,
'rate' => ['interval' => '1 hour'],
'window_mode' => 'calendar_aligned', // New hardened mode
]);
$consumption = $limiter->consume(1);
if (!$consumption->isAccepted()) {
return response()->json([
'error' => 'Rate limit exceeded',
'remaining' => $limiter->getRemainingTokens(),
'reserved' => $limiter->getPendingReservations(),
'window_mode' => $limiter->getWindowMode() // New: Expose window mode
], 429)
->header('Retry-After', $consumption->getRetryAfter()->format('U'));
}
// Process payment...
}
window_mode option for fixed_window policyfixed window mode if not specifiedRedisStorage (recommended), InMemoryStorage (dev), DatabaseStorage (custom)token_bucket (default, improved reservation handling)fixed_window (now supports calendar_aligned mode)sliding_window (with grace period support)// app/Http/Middleware/RateLimitMiddleware.php
public function handle($request, Closure $next, $key = null)
{
$key = $key ?? $this->resolveRateLimiterKey($request);
$config = config("rate_limits.{$key}");
$limiter = app('rate_limiter.factory')->create([
'id' => $key,
'policy' => $config['policy'],
'limit' => $config['limit'],
'rate' => $config['rate'],
'reservation_ttl' => $config['reservation_ttl'] ?? null,
'window_mode' => $config['window_mode'] ?? 'fixed',
]);
$consumption = $limiter->consume(1);
if (!$consumption->isAccepted()) {
return response()->json([
'error' => 'Rate limit exceeded',
'limit' => $limiter->getLimit(),
'remaining' => $limiter->getRemainingTokens(),
'reserved' => $limiter->getPendingReservations(),
'window_mode' => $limiter->getWindowMode(),
'next_window' => $limiter->getNextWindowStart()->format('c'),
], 429)
->header('Retry-After', $consumption->getRetryAfter()->format('U'));
}
return $next($request);
}
// Updated to use calendar-aligned fixed window
$limiter = app('rate_limiter.factory')->create([
'id' => 'user_' . auth()->id(),
'policy' => 'fixed_window',
'limit' => 10,
'rate' => ['interval' => '1 hour'],
'window_mode' => 'calendar_aligned', // Aligns to :00 minutes
]);
// New behavior: Windows reset at calendar boundaries (e.g., 1:00 PM instead of 1:00:00.000 PM)
$consumption = $limiter->consume(1);
if (!$consumption->isAccepted()) {
// Handle rate limit with precise window info
$nextWindow = $limiter->getNextWindowStart();
// ...
}
// app/Jobs/ProcessBatch.php
public function handle()
{
$limiter = app('rate_limiter.factory')->create([
'id' => 'batch_' . $this->batchId,
'policy' => 'fixed_window',
'limit' => 100,
'rate' => ['interval' => '1 day'],
'window_mode' => 'calendar_aligned', // Aligns to midnight
'reservation_ttl' => '2 hours',
]);
// Check window alignment before processing
$currentWindowStart = $limiter->getCurrentWindowStart();
$nextWindowStart = $limiter->getNextWindowStart();
// Reserve tokens with explicit TTL
$reservation = $lim
How can I help you explore Laravel packages today?