Rate Limiter

A "rate limiter" controls how frequently some event (e.g. an HTTP request or a login attempt) is allowed to happen. Rate limiting is commonly used as a defensive measure to protect services from excessive use (intended or not) and maintain their availability. It's also useful to control your internal or outbound processes (e.g. limit the number of simultaneously processed messages).

「レート リミッター」は、何らかのイベント (HTTP 要求やログイン試行など) の発生を許可する頻度を制御します。レート制限は、サービスを過剰な使用 (意図的かどうかに関係なく) から保護し、その可用性を維持するための防御手段として一般的に使用されます。また、内部または送信プロセスを制御するのにも役立ちます (たとえば、同時に処理されるメッセージの数を制限します)。

Symfony uses these rate limiters in built-in features like login throttling, which limits how many failed login attempts a user can make in a given period of time, but you can use them for your own features too.

Symfony はこれらのレート リミッターをログイン スロットリングなどの組み込み機能で使用します。これは、ユーザーが一定時間内に失敗できるログイン試行の回数を制限しますが、独自の機能にも使用できます。

Caution

注意

By definition, the Symfony rate limiters require Symfony to be booted in a PHP process. This makes them not useful to protect against DoS attacks. Such protections must consume the least resources possible. Consider using Apache mod_ratelimit, NGINX rate limiting or proxies (like AWS or Cloudflare) to prevent your server from being overwhelmed.

定義上、Symfony レート リミッターでは、Symfony を PHP プロセスで起動する必要があります。これにより、DoS 攻撃から保護するのに役立ちません。このような保護では、リソースの消費を最小限に抑える必要があります。サーバーが過負荷になるのを防ぐために、Apache mod_ratelimit、NGINX レート制限、またはプロキシ (AWS や Cloudflare など) の使用を検討してください。

Rate Limiting Policies

Symfony's rate limiter implements some of the most common policies to enforce rate limits: fixed window, sliding window, token bucket.

Symfony のレート リミッターは、固定ウィンドウ、スライディング ウィンドウ、トークン バケットなど、制限を実施するための最も一般的なポリシーのいくつかを実装しています。

Fixed Window Rate Limiter

This is the simplest technique and it's based on setting a limit for a given interval of time (e.g. 5,000 requests per hour or 3 login attempts every 15 minutes).

これは最も単純な手法であり、特定の時間間隔に制限を設定することに基づいています (たとえば、1 時間あたり 5,000 リクエストまたは 15 分ごとに 3 回のログイン試行)。

In the diagram below, the limit is set to "5 tokens per hour". Each window starts at the first hit (i.e. 10:15, 11:30 and 12:30). As soon as there are 5 hits (the blue squares) in a window, all others will be rejected (red squares).

下の図では、制限は「1 時間あたり 5 トークン」に設定されています。各ウィンドウは、最初のヒット (つまり、10:15、11:30、および 12:30) から始まります。ウィンドウに 5 つのヒット (青い四角) があるとすぐに、他のすべてのヒットは拒否されます (赤い四角)。

Its main drawback is that resource usage is not evenly distributed in time and it can overload the server at the window edges. In the previous example, there were 6 accepted requests between 11:00 and 12:00.

その主な欠点は、リソースの使用が時間的に均等に分散されず、ウィンドウの端でサーバーが過負荷になる可能性があることです.前の例では、11:00 から 12:00 の間に 6 つの要求が受け入れられました。

This is more significant with bigger limits. For instance, with 5,000 requests per hour, a user could make the 4,999 requests in the last minute of some hour and another 5,000 requests during the first minute of the next hour, making 9,999 requests in total in two minutes and possibly overloading the server. These periods of excessive usage are called "bursts".

これは、制限が大きいほど重要です。たとえば、1 時間あたり 5,000 件のリクエストがある場合、ユーザーはある時間の最後の 1 分間に 4,999 件のリクエストを作成し、次の 1 時間の最初の 1 分間にさらに 5,000 件のリクエストを作成し、2 分間で合計 9,999 件のリクエストを作成すると、サーバーが過負荷になる可能性があります。これらの過度の使用期間は「バースト」と呼ばれます。

Sliding Window Rate Limiter

The sliding window algorithm is an alternative to the fixed window algorithm designed to reduce bursts. This is the same example as above, but then using a 1 hour window that slides over the timeline:

スライディング ウィンドウ アルゴリズムは、固定ウィンドウ アルゴリズムに代わるもので、バーストを減らすように設計されています。これは上記と同じ例ですが、タイムライン上をスライドする 1 時間のウィンドウを使用しています。

As you can see, this removes the edges of the window and would prevent the 6th request at 11:45.

ご覧のとおり、これによりウィンドウの端が削除され、11:45 の 6 番目の要求が妨げられます。

To achieve this, the rate limit is approximated based on the current window and the previous window.

これを達成するために、レート制限は現在のウィンドウと前のウィンドウに基づいて概算されます。

For example: the limit is 5,000 requests per hour; a user made 4,000 requests the previous hour and 500 requests this hour. 15 minutes in to the current hour (25% of the window) the hit count would be calculated as: 75% * 4,000 + 500 = 3,500. At this point in time the user can only do 1,500 more requests.

例: 制限は 1 時間あたり 5,000 リクエストです。ユーザーは前の 1 時間に 4,000 件のリクエストを作成し、今時間は 500 件のリクエストを作成しました。現在の時間の 15 分 (ウィンドウの 25%) のヒット カウントは、75% * 4,000 + 500 = 3,500 として計算されます。この時点で、ユーザーはあと 1,500 のリクエストしか実行できません。

The math shows that the closer the last window is, the more the hit count of the last window will affect the current limit. This will make sure that a user can do 5,000 requests per hour but only if they are evenly spread out.

最後のウィンドウが近いほど、最後のウィンドウのヒット カウントが現在の制限に影響を与えることが数学的に示されています。これにより、ユーザーは 1 時間あたり 5,000 件のリクエストを実行できるようになりますが、それらが均等に分散されている場合に限られます。

Token Bucket Rate Limiter

This technique implements the token bucket algorithm, which defines continuously updating the budget of resource usage. It roughly works like this:

この手法は、リソース使用量の予算を継続的に更新することを定義するトークン バケット アルゴリズムを実装します。大まかに次のように動作します。
  • A bucket is created with an initial set of tokens;
    トークンの初期セットを使用してバケットが作成されます。
  • A new token is added to the bucket with a predefined frequency (e.g. every second);
    新しいトークンが事前定義された頻度 (例: 毎秒) でバケットに追加されます。
  • Allowing an event consumes one or more tokens;
    イベントを許可すると、1 つ以上のトークンが消費されます。
  • If the bucket still contains tokens, the event is allowed; otherwise, it's denied;
    バケットにまだトークンが含まれている場合、イベントは許可されます。それ以外の場合は拒否されます。
  • If the bucket is at full capacity, new tokens are discarded.
    バケットの容量がいっぱいになると、新しいトークンは破棄されます。

The below diagram shows a token bucket of size 4 that is filled with a rate of 1 token per 15 minutes:

以下の図は、15 分あたり 1 トークンのレートで満たされるサイズ 4 のトークン バケットを示しています。

This algorithm handles more complex back-off burst management. For instance, it can allow a user to try a password 5 times and then only allow 1 every 15 minutes (unless the user waits 75 minutes and they will be allowed 5 tries again).

このアルゴリズムは、より複雑なバックオフ バースト管理を処理します。たとえば、ユーザーがパスワードを 5 回試行することを許可し、その後 15 分ごとに 1 回だけ許可することができます (ユーザーが 75 分待ってから 5 回の再試行が許可される場合を除く)。

Installation

Before using a rate limiter for the first time, run the following command to install the associated Symfony Component in your application:

レート リミッターを初めて使用する前に、次のコマンドを実行して、関連する Symfony コンポーネントをアプリケーションにインストールします。
1
$ composer require symfony/rate-limiter

Configuration

The following example creates two different rate limiters for an API service, to enforce different levels of service (free or paid):

次の例では、API サービスに対して 2 つの異なるレート リミッターを作成し、異なるレベルのサービス (無料または有料) を適用します。
  • YAML
    YAML
  • XML
    XML
  • PHP
    PHP
1
2
3
4
5
6
7
8
9
10
11
12
# config/packages/rate_limiter.yaml
framework:
    rate_limiter:
        anonymous_api:
            # use 'sliding_window' if you prefer that policy
            policy: 'fixed_window'
            limit: 100
            interval: '60 minutes'
        authenticated_api:
            policy: 'token_bucket'
            limit: 5000
            rate: { interval: '15 minutes', amount: 500 }

Note

ノート

The value of the interval option must be a number followed by any of the units accepted by the PHP date relative formats (e.g. 3 seconds, 10 hours, 1 day, etc.)

interval オプションの値は、数値の後に PHP の日付相対形式で受け入れられる任意の単位 (例: 3 秒、10 時間、1 日など) を指定する必要があります。

In the anonymous_api limiter, after making the first HTTP request, you can make up to 100 requests in the next 60 minutes. After that time, the counter resets and you have another 100 requests for the following 60 minutes.

anonymous_api リミッターでは、最初の HTTP リクエストを行った後、次の 60 分間で最大 100 件のリクエストを行うことができます。その後、カウンターはリセットされ、次の 60 分間にさらに 100 件のリクエストがあります。

In the authenticated_api limiter, after making the first HTTP request you are allowed to make up to 5,000 HTTP requests in total, and this number grows at a rate of another 500 requests every 15 minutes. If you don't make that number of requests, the unused ones don't accumulate (the limit option prevents that number from being higher than 5,000).

authenticated_api リミッターでは、最初の HTTP リクエストを行った後、合計で最大 5,000 の HTTP リクエストを行うことが許可され、この数は 15 分ごとにさらに 500 リクエストの割合で増加します。その数のリクエストを行わないと、未使用のリクエストは蓄積されません (制限オプションにより、その数が 5,000 を超えることはありません)。

Rate Limiting in Action

After having installed and configured the rate limiter, inject it in any service or controller and call the consume() method to try to consume a given number of tokens. For example, this controller uses the previous rate limiter to control the number of requests to the API:

レート リミッターをインストールして構成したら、それをサービスまたはコントローラーに挿入し、consume() メソッドを呼び出して、指定された数のトークンを消費しようとします。たとえば、このコントローラーは以前のレート リミッターを使用して、API へのリクエスト数を制御します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// src/Controller/ApiController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Symfony\Component\RateLimiter\RateLimiterFactory;

class ApiController extends AbstractController
{
    // if you're using service autowiring, the variable name must be:
    // "rate limiter name" (in camelCase) + "Limiter" suffix
    public function index(Request $request, RateLimiterFactory $anonymousApiLimiter)
    {
        // create a limiter based on a unique identifier of the client
        // (e.g. the client's IP address, a username/email, an API key, etc.)
        $limiter = $anonymousApiLimiter->create($request->getClientIp());

        // the argument of consume() is the number of tokens to consume
        // and returns an object of type Limit
        if (false === $limiter->consume(1)->isAccepted()) {
            throw new TooManyRequestsHttpException();
        }

        // you can also use the ensureAccepted() method - which throws a
        // RateLimitExceededException if the limit has been reached
        // $limiter->consume(1)->ensureAccepted();

        // ...
    }

    // ...
}

Note

ノート

In a real application, instead of checking the rate limiter in all the API controller methods, create an event listener or subscriber for the kernel.request event and check the rate limiter once for all requests.

実際のアプリケーションでは、すべての API コントローラー メソッドでレート リミッターをチェックする代わりに、kernel.request イベントのイベント リスナーまたはサブスクライバーを作成し、すべてのリクエストに対してレート リミッターを 1 回チェックします。

Wait until a Token is Available

Instead of dropping a request or process when the limit has been reached, you might want to wait until a new token is available. This can be achieved using the reserve() method:

制限に達したときにリクエストまたはプロセスをドロップする代わりに、新しいトークンが利用可能になるまで待ちたい場合があります。これは、reserve() メソッドを使用して実現できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/Controller/ApiController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\RateLimiter\RateLimiterFactory;

class ApiController extends AbstractController
{
    public function registerUser(Request $request, RateLimiterFactory $authenticatedApiLimiter)
    {
        $apiKey = $request->headers->get('apikey');
        $limiter = $authenticatedApiLimiter->create($apiKey);

        // this blocks the application until the given number of tokens can be consumed
        $limiter->reserve(1)->wait();

        // optional, pass a maximum wait time (in seconds), a MaxWaitDurationExceededException
        // is thrown if the process has to wait longer. E.g. to wait at most 20 seconds:
        //$limiter->reserve(1, 20)->wait();

        // ...
    }

    // ...
}

The reserve() method is able to reserve a token in the future. Only use this method if you're planning to wait, otherwise you will block other processes by reserving unused tokens.

reserve() メソッドは、将来トークンを予約することができます。待機する予定がある場合にのみ、このメソッドを使用してください。そうしないと、未使用のトークンを予約して他のプロセスをブロックします。

Note

ノート

Not all strategies allow reserving tokens in the future. These strategies may throw a ReserveNotSupportedException when calling reserve().

すべての戦略が将来のトークンの予約を許可するわけではありません。これらの戦略は、reserve() の呼び出し時に ReserveNotSupportedException をスローする場合があります。

In these cases, you can use consume() together with wait(), but there is no guarantee that a token is available after the wait:

このような場合、消費()を待機()と一緒に使用できますが、待機後にトークンが使用可能になるという保証はありません:
1
2
3
4
5
// ...
do {
    $limit = $limiter->consume(1);
    $limit->wait();
} while (!$limit->isAccepted());

Exposing the Rate Limiter Status

When using a rate limiter in APIs, it's common to include some standard HTTP headers in the response to expose the limit status (e.g. remaining tokens, when new tokens will be available, etc.)

API でレート リミッターを使用する場合、制限ステータスを公開するために、いくつかの標準 HTTP ヘッダーを応答に含めるのが一般的です (例: 残りのトークン、いつ新しいトークンが利用可能になるかなど)。

Use the RateLimit object returned by the consume() method (also available via the getRateLimit() method of the Reservation object returned by the reserve() method) to get the value of those HTTP headers:

これらの HTTP ヘッダーの値を取得するには、consume() メソッドによって返される RateLimit オブジェクト (reserve() メソッドによって返される Reservation オブジェクトの getRateLimit() メソッドからも利用可能) を使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// src/Controller/ApiController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\RateLimiter\RateLimiterFactory;

class ApiController extends AbstractController
{
    public function index(Request $request, RateLimiterFactory $anonymousApiLimiter)
    {
        $limiter = $anonymousApiLimiter->create($request->getClientIp());
        $limit = $limiter->consume();
        $headers = [
            'X-RateLimit-Remaining' => $limit->getRemainingTokens(),
            'X-RateLimit-Retry-After' => $limit->getRetryAfter()->getTimestamp(),
            'X-RateLimit-Limit' => $limit->getLimit(),
        ];

        if (false === $limit->isAccepted()) {
            return new Response(null, Response::HTTP_TOO_MANY_REQUESTS, $headers);
        }

        // ...

        $response = new Response('...');
        $response->headers->add($headers);

        return $response;
    }
}

Storing Rate Limiter State

All rate limiter policies require to store their state (e.g. how many hits were already made in the current time window). By default, all limiters use the cache.rate_limiter cache pool created with the Cache component. This means that every time you clear the cache, the rate limiter will be reset.

すべてのレート リミッタ ポリシーは、その状態を保存する必要があります (たとえば、現在の時間枠で既に作成されたヒット数)。デフォルトでは、すべてのリミッターは Cache コンポーネントで作成された cache.rate_limiter キャッシュ プールを使用します。これは、キャッシュをクリアするたびに、レート リミッターがリセットされることを意味します。

You can use the cache_pool option to override the cache used by a specific limiter (or even create a new cache pool for it):

cache_pool オプションを使用して、特定のリミッターが使用するキャッシュをオーバーライドできます (または、新しいキャッシュ プールを作成することもできます)。
  • YAML
    YAML
  • XML
    XML
  • PHP
    PHP
1
2
3
4
5
6
7
8
# config/packages/rate_limiter.yaml
framework:
    rate_limiter:
        anonymous_api:
            # ...

            # use the "cache.anonymous_rate_limiter" cache pool
            cache_pool: 'cache.anonymous_rate_limiter'

Note

ノート

Instead of using the Cache component, you can also implement a custom storage. Create a PHP class that implements the StorageInterface and use the storage_service setting of each limiter to the service ID of this class.

Cache コンポーネントを使用する代わりに、customstorage を実装することもできます。 StorageInterface を実装する PHP クラスを作成し、各リミッターの storage_service 設定をこのクラスのサービス ID に使用します。

Using Locks to Prevent Race Conditions

Race conditions can happen when the same rate limiter is used by multiple simultaneous requests (e.g. three servers of a company hitting your API at the same time). Rate limiters use locks to protect their operations against these race conditions.

同じレート リミッターが複数の同時リクエストで使用されると、競合状態が発生する可能性があります (たとえば、会社の 3 つのサーバーが同時に API をヒットするなど)。レート リミッターはロックを使用して、これらの競合状態から操作を保護します。

By default, Symfony uses the global lock configured by framework.lock, but you can use a specific named lock via the lock_factory option (or none at all):

デフォルトでは、Symfony は Framework.lock によって設定されたグローバル ロックを使用しますが、lock_factory オプションを介して特定の名前付きロックを使用できます (またはまったく使用しません):
  • YAML
    YAML
  • XML
    XML
  • PHP
    PHP
1
2
3
4
5
6
7
8
9
10
11
# config/packages/rate_limiter.yaml
framework:
    rate_limiter:
        anonymous_api:
            # ...

            # use the "lock.rate_limiter.factory" for this limiter
            lock_factory: 'lock.rate_limiter.factory'

            # or don't use any lock mechanism
            lock_factory: null