@@ -9,7 +9,8 @@ Rate Limiter
9
9
A "rate limiter" controls how frequently some event (e.g. an HTTP request or a
10
10
login attempt) is allowed to happen. Rate limiting is commonly used as a
11
11
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).
13
14
14
15
Symfony uses these rate limiters in built-in features like "login throttling",
15
16
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
28
29
interval of time. For example: 5,000 requests per hour or 3 login attempts
29
30
every 15 minutes.
30
31
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.
34
37
35
38
Token Bucket Rate Limiter
36
39
~~~~~~~~~~~~~~~~~~~~~~~~~
37
40
38
41
This technique implements the `token bucket algorithm `_, which defines a
39
42
continuously updating budget of resource usage. It roughly works like this:
40
43
41
- * A bucket is initially created with zero or more tokens;
44
+ * A bucket is created with an initial set of tokens;
42
45
* A new token is added to the bucket with a predefined frequency (e.g. every second);
43
46
* Allowing an event consumes one or more tokens;
44
47
* 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):
65
68
# config/packages/rate_limiter.yaml
66
69
framework :
67
70
rate_limiter :
68
- anonymous_api_limiter :
71
+ anonymous_api :
69
72
strategy : fixed_window
70
73
limit : 100
71
- interval : 60m
72
- authenticated_api_limiter :
74
+ interval : ' 60 minutes '
75
+ authenticated_api :
73
76
strategy : token_bucket
74
77
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.)
76
85
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
78
87
make up to 100 requests in the next 60 minutes. After that time, the counter
79
88
resets and you have another 100 requests for the following 60 minutes.
80
89
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
82
91
are allowed to make up to 5,000 HTTP requests in total, and this number grows
83
92
at a rate of another 5,000 requests per hour. If you don't make that number of
84
93
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
92
101
of tokens. For example, this controller uses the previous rate limiter to control
93
102
the number of requests to the API::
94
103
95
- // src/Controller/LuckyController .php
104
+ // src/Controller/ApiController .php
96
105
namespace App\Controller;
97
106
98
107
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -101,15 +110,23 @@ the number of requests to the API::
101
110
102
111
class ApiController extends AbstractController
103
112
{
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 )
106
115
{
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
+
107
120
// 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() ) {
110
123
throw new TooManyRequestsHttpException();
111
124
}
112
125
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
+
113
130
// ...
114
131
}
115
132
@@ -131,10 +148,12 @@ token is available. In those cases, use the ``reserve()`` and ``wait()`` methods
131
148
132
149
class ApiController extends AbstractController
133
150
{
134
- public function registerUser(LimiterInterface $authenticated_api_limiter )
151
+ public function registerUser(LimiterInterface $authenticatedApiLimiter )
135
152
{
153
+ $limiter = $authenticatedApiLimiter->create($request->getClientIp());
154
+
136
155
// 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();
138
157
139
158
// ...
140
159
}
@@ -158,7 +177,11 @@ Symfony application. If you prefer to change that, use the ``lock`` and
158
177
# ...
159
178
# the value is the name of any cache pool defined in your application
160
179
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'
161
183
# the value is the name of any lock defined in your application
162
184
lock : ' app.rate_limiter_lock'
163
185
164
186
.. _`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