Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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

PolicyHow it counts
fixed_windowNo more than N actions in each time slot. Simple, but a double burst is possible where two slots meet
sliding_windowSmooths the boundary: it accounts for the previous slot too
token_bucketA "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.

Thanks for reading!

Donate to the site — we work hard for you.