Rate limiting
A contact form can be flooded with spam, an API overloaded with requests, a confirmation code guessed by brute force. The RateLimiter component counts actions and refuses when there are too many.
Installation and configuration
composer require symfony/rate-limiter# config/packages/rate_limiter.yaml
framework:
rate_limiter:
contact_form:
policy: 'fixed_window'
limit: 5
interval: '10 minutes'The contact_form limiter allows five actions in ten minutes.
Using it
Symfony creates a factory service whose argument name is the limiter's name plus the word Limiter:
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Symfony\Component\RateLimiter\RateLimiterFactory;
#[Route('/contact', name: 'contact', methods: ['POST'])]
public function send(Request $request, RateLimiterFactory $contactFormLimiter): Response
{
$limiter = $contactFormLimiter->create($request->getClientIp());
if (!$limiter->consume(1)->isAccepted()) {
throw new TooManyRequestsHttpException();
}
// sending the message
}The argument of create() is the key the count is kept by: the visitor's address, a user identifier, an email address.
Policies
| Policy | How it counts |
|---|---|
fixed_window | No more than N actions in each time slot. Simple, but a double burst is possible where two slots meet |
sliding_window | Smooths the boundary: it accounts for the previous slot too |
token_bucket | A "bucket of tokens": allows a short burst, then refills at a constant rate |
framework:
rate_limiter:
api:
policy: 'token_bucket'
limit: 100
rate: { interval: '1 minute', amount: 20 }A client can make 100 requests at once and then gets 20 new ones a minute.
Response headers
A polite API tells the client how many requests remain and when to retry:
$limit = $limiter->consume(1);
$headers = [
'X-RateLimit-Remaining' => $limit->getRemainingTokens(),
'X-RateLimit-Limit' => $limit->getLimit(),
'Retry-After' => $limit->getRetryAfter()->getTimestamp() - time(),
];
if (!$limit->isAccepted()) {
return new JsonResponse(['error' => 'Too many requests'], 429, $headers);
}Limiting by user
$key = $this->getUser()?->getUserIdentifier() ?? $request->getClientIp();
$limiter = $apiLimiter->create($key);Logged-in users are counted by name, guests by address.
Where the counters are stored
By default — in the application cache. With several servers the cache must be shared, otherwise each server keeps its own count:
framework:
rate_limiter:
api:
policy: 'sliding_window'
limit: 60
interval: '1 minute'
cache_pool: 'cache.rate_limiter'The cache.rate_limiter pool itself is pointed at Redis in cache.yaml.
Logging in
A login form needs no code of its own: the login_throttling setting in security.yaml uses this same component.
Common mistake
If the site sits behind a proxy or a load balancer, getClientIp() returns the proxy's address — one for everybody. The limit trips for all visitors at once. Name the trusted proxies in the framework.trusted_proxies setting so that Symfony takes the real address from the X-Forwarded-For header.