Skip to content

Commit d90991e

Browse files
committed
[RateLimiter] Added the docs for the new component
1 parent 707580e commit d90991e

File tree

2 files changed

+196
-0
lines changed

2 files changed

+196
-0
lines changed

index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Topics
5252
notifier
5353
performance
5454
profiler
55+
rate_limiter
5556
routing
5657
security
5758
session

rate_limiter.rst

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
Rate Limiter
2+
============
3+
4+
.. versionadded:: 5.2
5+
6+
The RateLimiter component was introduced in Symfony 5.2 as an
7+
:doc:`experimental feature </contributing/code/experimental>`.
8+
9+
A "rate limiter" controls how frequently some event (e.g. an HTTP request or a
10+
login attempt) is allowed to happen. Rate limiting is commonly used as a
11+
defensive measure to protect services from excessive use (intended or not) and
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).
14+
15+
Symfony uses these rate limiters in built-in features like "login throttling",
16+
which limits how many failed login attempts a user can make in a given period of
17+
time, but you can use them for your own features too.
18+
19+
Rate Limiting Strategies
20+
------------------------
21+
22+
Symfony's rate limiter implements two of the most common strategies to enforce
23+
rate limits: **fixed window** and **token bucket**.
24+
25+
Fixed Window Rate Limiter
26+
~~~~~~~~~~~~~~~~~~~~~~~~~
27+
28+
This is the simplest technique and it's based on setting a limit for a given
29+
interval of time. For example: 5,000 requests per hour or 3 login attempts
30+
every 15 minutes.
31+
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.
37+
38+
Token Bucket Rate Limiter
39+
~~~~~~~~~~~~~~~~~~~~~~~~~
40+
41+
This technique implements the `token bucket algorithm`_, which defines a
42+
continuously updating budget of resource usage. It roughly works like this:
43+
44+
* A bucket is created with an initial set of tokens;
45+
* A new token is added to the bucket with a predefined frequency (e.g. every second);
46+
* Allowing an event consumes one or more tokens;
47+
* If the bucket still contains tokens, the event is allowed; otherwise, it's denied;
48+
* If the bucket is at full capacity, new tokens are discarded.
49+
50+
Installation
51+
------------
52+
53+
Before using a rate limiter for the first time, run the following command to
54+
install the associated Symfony Component in your application:
55+
56+
.. code-block:: terminal
57+
58+
$ composer require symfony/rate-limiter
59+
60+
Configuration
61+
-------------
62+
63+
The following example creates two different rate limiters for an API service, to
64+
enforce different levels of service (free or paid):
65+
66+
.. code-block:: yaml
67+
68+
# config/packages/rate_limiter.yaml
69+
framework:
70+
rate_limiter:
71+
anonymous_api:
72+
strategy: fixed_window
73+
limit: 100
74+
interval: '60 minutes'
75+
authenticated_api:
76+
strategy: token_bucket
77+
limit: 5000
78+
rate: { interval: '15 minutes', amount: 500 }
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.)
85+
86+
In the ``anonymous_api`` limiter, after making the first HTTP request, you can
87+
make up to 100 requests in the next 60 minutes. After that time, the counter
88+
resets and you have another 100 requests for the following 60 minutes.
89+
90+
In the ``authenticated_api`` limiter, after making the first HTTP request you
91+
are allowed to make up to 5,000 HTTP requests in total, and this number grows
92+
at a rate of another 500 requests every 15 minutes. If you don't make that
93+
number of requests, the unused ones don't accumulate (the ``limit`` option
94+
prevents that number from being higher than 5,000).
95+
96+
Rate Limiting in Action
97+
-----------------------
98+
99+
After having installed and configured the rate limiter, inject it in any service
100+
or controller and call the ``consume()`` method to try to consume a given number
101+
of tokens. For example, this controller uses the previous rate limiter to control
102+
the number of requests to the API::
103+
104+
// src/Controller/ApiController.php
105+
namespace App\Controller;
106+
107+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
108+
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
109+
use Symfony\Component\RateLimiter\Limiter;
110+
111+
class ApiController extends AbstractController
112+
{
113+
// the variable name must be: "rate limiter name" + "limiter" suffix
114+
public function index(Limiter $anonymousApiLimiter)
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+
120+
// the argument of consume() is the number of tokens to consume
121+
// and returns an object of type Limit
122+
if (false === $anonymous_api_limiter->consume(1)->isAccepted()) {
123+
throw new TooManyRequestsHttpException();
124+
}
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+
130+
// ...
131+
}
132+
133+
// ...
134+
}
135+
136+
.. note::
137+
138+
In a real application, instead of checking the rate limiter in all the API
139+
controller methods, create an :doc:`event listener or subscriber </event_dispatcher>`
140+
for the :ref:`kernel.request event <component-http-kernel-kernel-request>`
141+
and check the rate limiter once for all requests.
142+
143+
In other scenarios you may want instead to wait as long as needed until a new
144+
token is available. In those cases, use the ``wait()`` method::
145+
146+
// src/Controller/ApiController.php
147+
namespace App\Controller;
148+
149+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
150+
use Symfony\Component\HttpFoundation\Request;
151+
use Symfony\Component\RateLimiter\Limiter;
152+
153+
class ApiController extends AbstractController
154+
{
155+
public function registerUser(Request $request, Limiter $authenticatedApiLimiter)
156+
{
157+
$apiKey = $request->headers->get('apikey');
158+
$limiter = $authenticatedApiLimiter->create($apiKey);
159+
160+
// this blocks the application until the given number of tokens can be consumed
161+
do {
162+
$limit = $limiter->consume(1);
163+
$limit->wait();
164+
} while (!$limit->isAccepted());
165+
166+
// ...
167+
}
168+
169+
// ...
170+
}
171+
172+
Rate Limiter Storage and Locking
173+
--------------------------------
174+
175+
Rate limiters use the default cache and locking mechanisms defined in your
176+
Symfony application. If you prefer to change that, use the ``lock`` and
177+
``storage`` options:
178+
179+
.. code-block:: yaml
180+
181+
# config/packages/rate_limiter.yaml
182+
framework:
183+
rate_limiter:
184+
anonymous_api_limiter:
185+
# ...
186+
# the value is the name of any cache pool defined in your application
187+
cache_pool: 'app.redis_cache'
188+
# or define a service implementing StorageInterface to use a different
189+
# mechanism to store the limiter information
190+
storage: 'App\RateLimiter\CustomRedisStorage'
191+
# the value is the name of any lock defined in your application
192+
lock: 'app.rate_limiter_lock'
193+
194+
.. _`token bucket algorithm`: https://en.wikipedia.org/wiki/Token_bucket
195+
.. _`PHP date relative formats`: https://www.php.net/datetime.formats.relative

0 commit comments

Comments
 (0)