Rate Limiting Controllers with the #[RateLimit] Attribute
The RateLimiter component lets you control how often an action can happen, but
until now you had to inject a limiter factory into your controller and write the
boilerplate code needed to build the key, consume a token and return a 429 response.
Symfony 8.1 adds a #[RateLimit] attribute that does all of this declaratively.
It references a limiter configured under framework.rate_limiter and, when the
limit is exceeded, the kernel automatically returns a 429 Too Many Requests
response with a Retry-After header:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\HttpKernel\Attribute\RateLimit;
class ApiController extends AbstractController
{
// by default, the bucket is keyed by client IP + HTTP method + path
#[RateLimit('api')]
public function index(): JsonResponse { /* ... */ }
// restrict the limit to specific HTTP methods
#[RateLimit('api', methods: ['POST', 'PUT'])]
public function edit(): JsonResponse { /* ... */ }
// build the bucket key from a submitted field (a closure also works)
#[RateLimit('per_account', key: new Expression('request.request.get("email")'))]
public function resetPassword(): Response { /* ... */ }
// consume several tokens per request for expensive endpoints
#[RateLimit('api', tokens: 5)]
public function export(): JsonResponse { /* ... */ }
}
The attribute is repeatable, so you can stack multiple limits on the same action (all of them must pass), and you can also apply it to the controller class to rate limit every action in it.
Calendar-Aligned Fixed Windows
The fixed_window policy counts hits in fixed time windows, but those windows
start on the first request. If you allow 10,000 calls per month and the first one
arrives on the 18th, the counter resets on the 18th of the following month instead
of at the beginning of the next calendar month.
Symfony 8.1 adds an anchor_at option that aligns fixed_window limiters to
a calendar. Windows then reset at anchor_at + n × interval, which is exactly
what you need for billing cycles, fiscal years, or fixed-day weekly resets:
1 2 3 4 5 6 7 8 9
# config/packages/rate_limiter.yaml
framework:
rate_limiter:
api_quota:
policy: 'fixed_window'
limit: 10000
interval: '1 month'
# the counter now resets on the 5th of every month at 00:00 UTC
anchor_at: '2026-01-05 00:00:00 UTC'
The anchor_at value can be any string accepted by DateTimeImmutable.
Symfony computes each window by repeatedly adding the interval to it. This option
only works with the fixed_window policy and an interval of at least one month.