|
| 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