Rate Limiting Controllers with the #[RateLimit] Attribute

Ayyoub AFW-ALLAH
Contributed by Ayyoub AFW-ALLAH in #63907

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

Thibault Gattolliat
Contributed by Thibault Gattolliat in #62127

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.

Published in #Living on the edge