【Laravel】rate limiting(レート制限)の実装方法|RateLimiter・throttleミドルウェア

はじめに

「ログイン API に 1 分あたり 5 回までの上限を設けたい」「ファイルアップロードだけユーザー ID ごとに上限を変えたい」といった要件は、Web アプリ開発でよく出てきます。同一 IP からの連続アクセスを抑えたい場面も多く、短時間の大量リクエストはサーバー負荷やブルートフォース攻撃のリスクにつながります。

Laravel では RateLimiter でレートリミッターを登録し、throttle ミドルウェアでルートへ適用します。throttle はルート到達前にカウンタを更新します。上限超過時はコントローラ実行前に HTTP 429 Too Many Requests を返します。手動でカウンタを管理するより、ミドルウェア層で共通化したほうが、ルート定義からレートリミッターの内容を把握しやすくなります。

レートリミッターの適用は、大きく次の 2 段階で構成されます。

  1. RateLimiter::for で名前付きレートリミッターを登録する
  2. throttle ミドルウェアでルートにレートリミッターを適用する

検証環境の前提: コード例は Laravel 11 以降の構成を主な前提としています。登録は AppServiceProviderboot、ミドルウェア設定は bootstrap/app.php を想定しています。挙動の細部はメジャーバージョンで変わることがあります。手元の composer.jsonlaravel/framework に合わせ、Routing の Rate Limiting 節を参照してください。

この記事では次の範囲を整理します。

  • RateLimiterLimit クラスの基本
  • throttle ミドルウェアの適用方法
  • キー設計(IP・ユーザー ID・メールアドレスなど)
  • 実装時の注意点とよくある失敗例

背景:レートリミッターが担う役割

レートリミッターは、一定時間あたりのリクエスト数に上限を設ける仕組みです。Laravel では cache と同じストアにカウンタを保存し、上限を超えたリクエストには 429 を返します。

RateLimiter::for('api', …) は名前付きレートリミッターの登録、Limit::perMinute(60) はその中身(時間枠と上限回数)を表します。

要素役割
RateLimiter::for('名前', ...)レートリミッターの定義と登録
Limit::perMinute(n) など時間枠と上限回数の指定
->by($key)カウンタを分ける単位(IP、ユーザー ID など)
throttle:名前 ミドルウェアルートへの適用

カウンタの状態はキャッシュに保存されます。config/cache.phpdefault ドライバ(fileredis など)がそのまま使われるため、本番環境ではキャッシュの可用性がレートリミッターの信頼性に直結します。

実装

レートリミッターの定義(RateLimiter::for

App\\Providers\\AppServiceProviderboot メソッドで、名前付きのレートリミッターを登録します。アプリ起動時に 1 回だけ実行されるため、ここにレートリミッターを集約するのが一般的です。公式ドキュメントでも同じ場所が案内されています。

<?php

namespace App\\Providers;

use Illuminate\\Cache\\RateLimiting\\Limit;
use Illuminate\\Http\\Request;
use Illuminate\\Support\\Facades\\RateLimiter;
use Illuminate\\Support\\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        RateLimiter::for('api', function (Request $request) {
            return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
        });
    }
}

Limit クラスには perMinuteperHourperDay などのビルダーメソッドがあります。by() にキーを渡すと、カウンタが分かれます。代表例は $request->user()->id$request->ip() です。by() を省略すると、全クライアントでカウンタを共有します。

キー設計(by の選び方)

by() の引数がカウンタを分ける単位です。ルートの性質に合わせて選びます。

キー向いている場面注意点
$request->ip()未認証の公開 APINAT 配下では多数ユーザーが同一 IP になる
$request->user()->id認証済み API未認証時は別のキーへフォールバックが必要
$request->input('email')ログイン・パスワードリセット入力値の存在を前提にする

ログインのようにブルートフォース対策が目的の場合は、メールアドレスをキーに細かくレートリミッターを適用するパターンがよく使われます。

RateLimiter::for('login', function (Request $request) {
    return [
        Limit::perMinute(30),
        Limit::perMinute(5)->by($request->input('email')),
    ];
});

配列を返すと複数のレートリミッターが順に評価されます。上の例では、同一 IP から 1 分に 30 回を超えると 429 になります。IP 内ではさらに、同一メールアドレスへの試行が 1 分 5 回を超えると 429 になります。

ルートへの適用(throttle ミドルウェア)

定義した名前を throttle ミドルウェアに渡してルートに適用します。

use Illuminate\\Support\\Facades\\Route;

Route::middleware(['throttle:api'])->group(function () {
    Route::get('/status', fn () => ['ok' => true]);
});

Route::post('/login', [AuthController::class, 'login'])
    ->middleware('throttle:login');

routes/api.php のルートには、プロジェクトによっては api ミドルウェアグループに throttle:api が含まれていることもあります。php artisan route:list -vv で Middleware 列を確認し、同じ throttle:api が 2 行出ていないかを見ると、二重適用の有無を把握しやすくなります。

シンプルなケースでは、名前付きレートリミッターを使わず次のように直接指定する書き方もあります。

Route::get('/health', fn () => 'ok')->middleware('throttle:60,1');

throttle:60,1 は「最大 60 回 / 1 分」を意味します。同じレートリミッターを複数ルートで使う場合は RateLimiter::for で名前を付けておくほうが、変更箇所を 1 か所にまとめやすくなります。

上限超過時のレスポンス

上限を超えたリクエストには、Laravel が自動的に 429 レスポンスを返します。カスタムレスポンスが必要な場合は Limitresponse メソッドを使います。

RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)
        ->by($request->user()?->id ?: $request->ip())
        ->response(function (Request $request, array $headers) {
            return response()->json([
                'message' => 'リクエストが多すぎます。しばらく待ってから再試行してください。',
            ], 429, $headers);
        });
});

レスポンスヘッダには X-RateLimit-LimitRetry-After などが含まれることがあります。利用するヘッダ名は Laravel のバージョンやミドルウェア実装に依存するため、実際のレスポンスを確認するのが確実です。

curl -i <http://localhost/api/status>

応用

Redis を使う場合

キャッシュドライバに Redis を使っている場合、Laravel 11 のミドルウェア設定ファイル bootstrap/app.phpthrottleWithRedis() を呼べます。withMiddleware 内に書くと、throttle ミドルウェアが Redis ベースの実装へ切り替わります。詳細は公式ドキュメントの Throttling With Redisを参照してください。

->withMiddleware(function (Middleware $middleware): void {
    $middleware->throttleWithRedis();
})

複数サーバーでアプリを水平スケールする構成では、ファイルキャッシュだとカウンタがサーバー間で共有されません。Redis などの共有ストアを検討する場面が多いです。

レスポンスに基づくカウンタ更新(after メソッド)

通常の throttle はリクエストごとにカウンタを更新します。after メソッドを使うと、レスポンスの内容に応じてカウンタを更新するかどうかを制御できます。存在しない ID の総当たり(404 のみカウンタ対象)を抑えたいときなどに向いています。

use Symfony\\Component\\HttpFoundation\\Response;

RateLimiter::for('resource-lookup', function (Request $request) {
    return Limit::perMinute(20)
        ->by($request->ip())
        ->after(function (Response $response) {
            return $response->getStatusCode() === 404;
        });
});

実装時の注意点

キー設計を先に決める。

by($request->ip()) だけでは、NAT 配下の多数ユーザーが同一カウンタを共有しやすくなります。1 人でも上限に達すれば、同じ IP への他ユーザーアクセスは 429 となります。

キャッシュドライバと本番構成を合わせる。 レートリミッターはキャッシュに依存します。ステージングと本番でドライバが異なると、カウンタの挙動も変わります。水平スケール時は共有キャッシュの有無を確認します。

ミドルウェアの二重適用に注意する。 api グループとルート個別の両方に throttle が付いていると、意図より厳しいレートリミッターになることがあります。route:list -vv で適用ミドルウェアを確認してからデプロイするのが安全です。

認証前のルートではユーザー ID に頼らない。 ログイン前のルートで by($request->user()->id) と書くと、未認証時は null がキーになります。別ユーザーが同じカウンタを共有する可能性があるため、次のようにフォールバックを書きます。

// 避けたい例(未認証時に null キー)
Limit::perMinute(10)->by($request->user()->id);

// 推奨例
Limit::perMinute(10)->by($request->user()?->id ?: $request->ip());

失敗例:throttle:login を定義せずにルートだけ付けた

ルートに ->middleware('throttle:login') を付けたものの、AppServiceProvider 側の登録を忘れたケースです。RateLimiter::for('login', ...) が未定義のままです。Laravel は存在しないレートリミッター名を解決できず、リクエスト処理中に例外が発生します。

名前付きレートリミッターを使う場合は、定義(RateLimiter::for)と適用(throttle:名前)をセットで管理します。チーム開発では、レートリミッター名を定数化するか、定義ファイルを 1 か所にまとめると見落としを減らせます。

学び:レートリミッターを HTTP 入口で宣言する

レートリミッターは、ビジネスロジックというより HTTP 入口でのトラフィック制御に近い関心事です。RateLimiter でレートリミッターを名前付きで定義し、throttle でルートに宣言的に付けることで、「このルートはどれだけの頻度まで許容するか」がコードレビューで追いやすくなります。

ログインのように login という名前でレートリミッターを宣言しておけば、上限値の変更も AppServiceProvider の 1 か所で議論できます。攻撃面になりやすいルートから先に適用し、アクセスログの 429 件数などを見ながら調整する進め方が現実的です。

まとめ

  • RateLimiter::for で名前付きレートリミッターを登録し、Limit で時間枠と上限を指定する。
  • throttle:名前 ミドルウェアでルートに適用し、超過時は 429 が返る。
  • キー設計(IP・ユーザー ID・入力値)とキャッシュドライバの選択が、本番での挙動を左右する。

次に試せること

  1. AppServiceProviderapi 用のレートリミッターを定義し、上限を 3 回など低い値に設定する。次のコマンドで 4 回目以降が 429 になることを確認する。
for i in $(seq 1 5); do
  curl -s -o /dev/null -w "%{http_code}\\n" <http://localhost/api/status>
done
  1. ログインルート用に login レートリミッターを作り、同じ email で 6 回 POST /login すると 429 になることを試す。
  2. php artisan route:list -vv で Middleware 列に throttle が重複していないか確認する。

Source