Skip to content

Commit 1eb70c0

Browse files
committed
Fixes and improvements
1 parent ba898a6 commit 1eb70c0

File tree

1 file changed

+41
-18
lines changed

1 file changed

+41
-18
lines changed

rate_limiter.rst

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ Rate Limiter
99
A "rate limiter" controls how frequently some event (e.g. an HTTP request or a
1010
login attempt) is allowed to happen. Rate limiting is commonly used as a
1111
defensive measure to protect services from excessive use (intended or not) and
12-
maintain their availability.
12+
maintain their availability. It's also useful to control your internal or
13+
outbound processes (e.g. limit the number of simultaneously processed messages).
1314

1415
Symfony uses these rate limiters in built-in features like "login throttling",
1516
which limits how many failed login attempts a user can make in a given period of
@@ -28,17 +29,19 @@ This is the simplest technique and it's based on setting a limit for a given
2829
interval of time. For example: 5,000 requests per hour or 3 login attempts
2930
every 15 minutes.
3031

31-
Its main drawback is that resource usage is not evenly distributed in time. In
32-
the previous example, a user could make the 5,000 requests in the first minute
33-
(possibly overloading the server) and do nothing for the rest of the hour.
32+
Its main drawback is that resource usage is not evenly distributed in time and
33+
it can overload the server at the window edges. In the previous example, a user
34+
could make the 4,999 requests in the last minute of some hour and another 5,000
35+
requests during the first minute of the next hour, making 9,999 requests in
36+
total in two minutes and possibly overloading the server.
3437

3538
Token Bucket Rate Limiter
3639
~~~~~~~~~~~~~~~~~~~~~~~~~
3740

3841
This technique implements the `token bucket algorithm`_, which defines a
3942
continuously updating budget of resource usage. It roughly works like this:
4043

41-
* A bucket is initially created with zero or more tokens;
44+
* A bucket is created with an initial set of tokens;
4245
* A new token is added to the bucket with a predefined frequency (e.g. every second);
4346
* Allowing an event consumes one or more tokens;
4447
* If the bucket still contains tokens, the event is allowed; otherwise, it's denied;
@@ -65,20 +68,26 @@ enforce different levels of service (free or paid):
6568
# config/packages/rate_limiter.yaml
6669
framework:
6770
rate_limiter:
68-
anonymous_api_limiter:
71+
anonymous_api:
6972
strategy: fixed_window
7073
limit: 100
71-
interval: 60m
72-
authenticated_api_limiter:
74+
interval: '60 minutes'
75+
authenticated_api:
7376
strategy: token_bucket
7477
limit: 5000
75-
rate: { interval: 1h, amount: 5000 }
78+
rate: { interval: '1 hour', amount: 5000 }
79+
80+
.. note::
81+
82+
The value of the ``interval`` option must be a number followed by any of the
83+
units accepted by the `PHP date relative formats`_ (e.g. ``3 seconds``,
84+
``10 hours``, ``1 day``, etc.)
7685

77-
In the ``anonymous_api_limiter``, when you make the first HTTP request, you can
86+
In the ``anonymous_api`` limiter, after making the first HTTP request, you can
7887
make up to 100 requests in the next 60 minutes. After that time, the counter
7988
resets and you have another 100 requests for the following 60 minutes.
8089

81-
In the ``authenticated_api_limiter``, when you make the first HTTP request you
90+
In the ``authenticated_api`` limiter, after making the first HTTP request you
8291
are allowed to make up to 5,000 HTTP requests in total, and this number grows
8392
at a rate of another 5,000 requests per hour. If you don't make that number of
8493
requests, the unused ones don't accumulate (the ``limit`` option prevents that
@@ -92,7 +101,7 @@ or controller and call the ``consume()`` method to try to consume a given number
92101
of tokens. For example, this controller uses the previous rate limiter to control
93102
the number of requests to the API::
94103

95-
// src/Controller/LuckyController.php
104+
// src/Controller/ApiController.php
96105
namespace App\Controller;
97106

98107
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -101,15 +110,23 @@ the number of requests to the API::
101110

102111
class ApiController extends AbstractController
103112
{
104-
// the variable name must be the exact same as the rate limiter name
105-
public function index(LimiterInterface $anonymous_api_limiter)
113+
// the variable name must be: "rate limiter name" + "limiter" suffix
114+
public function index(LimiterInterface $anonymousApiLimiter)
106115
{
116+
// create a limiter based on a unique identifier of the client
117+
// (e.g. the client's IP address, a username/email, an API key, etc.)
118+
$limiter = $anonymousApiLimiter->create($request->getClientIp());
119+
107120
// the argument of consume() is the number of tokens to consume
108-
// and returns FALSE if they cannot be consumed
109-
if (false === $anonymous_api_limiter->consume(1)) {
121+
// and returns an object of type Limit
122+
if (false === $anonymous_api_limiter->consume(1)->isAccepted()) {
110123
throw new TooManyRequestsHttpException();
111124
}
112125

126+
// you can also use the ensureAccepted() method - which throws a
127+
// RateLimitExceededException if the limit has been reached
128+
// $limiter->consume(1)->ensureAccepted();
129+
113130
// ...
114131
}
115132

@@ -131,10 +148,12 @@ token is available. In those cases, use the ``reserve()`` and ``wait()`` methods
131148

132149
class ApiController extends AbstractController
133150
{
134-
public function registerUser(LimiterInterface $authenticated_api_limiter)
151+
public function registerUser(LimiterInterface $authenticatedApiLimiter)
135152
{
153+
$limiter = $authenticatedApiLimiter->create($request->getClientIp());
154+
136155
// this blocks the application until the given number of tokens can be consumed
137-
$authenticated_api_limiter->reserve(1)->wait();
156+
$limiter->consume(1)->wait();
138157

139158
// ...
140159
}
@@ -158,7 +177,11 @@ Symfony application. If you prefer to change that, use the ``lock`` and
158177
# ...
159178
# the value is the name of any cache pool defined in your application
160179
storage: 'app.redis_cache'
180+
# or define a service implementing StorageInterface to use a different
181+
# mechanism to store the limiter information
182+
storage: 'App\RateLimiter\CustomRedisStorage'
161183
# the value is the name of any lock defined in your application
162184
lock: 'app.rate_limiter_lock'
163185
164186
.. _`token bucket algorithm`: https://en.wikipedia.org/wiki/Token_bucket
187+
.. _`PHP date relative formats`: https://www.php.net/datetime.formats.relative

0 commit comments

Comments
 (0)