対象バージョン: Laravel 13.x / PHP 8.3+(laravel/framework v13.42.0 で動作確認)
Laravel では、同じ操作や HTTP リクエストが短時間に繰り返されないよう、レート制限をかけられます。この記事は、RateLimiter ファサードで試行回数を数えたい方や、ルートに throttle ミドルウェアを付けて 429 を返したい方向けです。Facade の基本、名前付きリミッターの定義、ミドルウェアへの紐づけ、Feature テストまでを順に説明します。レート制限はキャッシュストアを使うため、キャッシュの基礎は【PHP】Laravelでキャッシュを実装する方法を参照してください。例外の報告自体を間引く $exceptions->throttle(...) は、HTTP の RateLimiter::for / throttle ミドルウェアとは別物です。例外報告側は【PHP】Laravelでエラー処理(例外の報告・描画とカスタムエラーページ)を実装する方法で扱っています。
この記事で分かること
RateLimiter::attempt/remaining/clearなどの Facade APIincrementによる手動カウントと、ドキュメント上の競合への注意- レート制限が使うキャッシュストア(
CACHE_STORE) RateLimiter::forとLimit::perMinute(...)->by(...)による名前付きリミッターthrottle:nameミドルウェアでのルート制限と 429 レスポンス- Feature テストでの確認方法
前提
Laravel 13.x は PHP 8.3 以上が必要です。動作確認は、composer create-project laravel/laravel:^13.0 で新規作成したアプリ(laravel/laravel v13.11.0、laravel/framework v13.42.0、PHP 8.4.27)で行いました。プロジェクトの作成方法は【PHP】Laravel入門:インストールから簡単なサンプルアプリケーションの作成を参照してください。
この記事の HTTP 検証では、確認用に名前付きリミッターを手動登録しています。install:api が用意する既定の API リミッターは参考情報にとどめ、ここでは扱いません。API ルート全体への適用例は【PHP】LaravelでRESTful APIを作成する方法とあわせて読むと分かりやすいです。認証済みユーザー単位のキー($request->user())を考える場合は、【PHP】Laravel SanctumでAPIトークン認証を実装する方法も参照してください。
RateLimiter ファサードの基本
Illuminate\Support\Facades\RateLimiter は、内部で Illuminate\Cache\RateLimiter を使います。よく使うメソッドは attempt / tooManyAttempts / increment / attempts / remaining / availableIn / clear、および後述の for です。
attempt は、残回数があるあいだコールバックを実行してその戻り値(または true)を返し、残が無いときは false を返します。減衰(decay)の既定は 60 秒です。確認環境(キャッシュを array に設定)での例です。
<?php
use Illuminate\Support\Facades\RateLimiter;
$key = 'verify-send:user-1';
RateLimiter::clear($key);
RateLimiter::remaining($key, 3); // 3
$ok1 = RateLimiter::attempt($key, 3, fn () => 'done-1', 60);
$ok2 = RateLimiter::attempt($key, 3, fn () => 'done-2', 60);
$ok3 = RateLimiter::attempt($key, 3, fn () => 'done-3', 60);
$ok4 = RateLimiter::attempt($key, 3, fn () => 'done-4', 60); // false
RateLimiter::tooManyAttempts($key, 3); // true
RateLimiter::remaining($key, 3); // 0
RateLimiter::attempts($key); // 3
RateLimiter::availableIn($key); // 60
RateLimiter::clear($key);
4 回目の attempt は false になり、tooManyAttempts は true、availableIn は 60(秒)でした。clear のあとは再び残回数 3 に戻ります。
手動で回数を増やす
tooManyAttempts と increment を組み合わせて、自分で回数を進めることもできます。
<?php
use Illuminate\Support\Facades\RateLimiter;
$key2 = 'verify-manual:ip';
RateLimiter::clear($key2);
RateLimiter::increment($key2, 60); // 1
RateLimiter::increment($key2, 60); // 2
RateLimiter::remaining($key2, 2); // 0
RateLimiter::tooManyAttempts($key2, 2); // true
ドキュメントでは、tooManyAttempts と increment を分けて呼ぶと並行実行時に競合しうると注意されています。redis / memcached / database ストアでは、原子性のために increment() の戻り値を見る方法が推奨されています。この記事の確認は array キャッシュ上の単一プロセスでの動作です。
キャッシュストアとの関係
レート制限は、アプリの既定キャッシュストアを使います。スケルトンの config/cache.php では次のとおりです(該当行)。
'default' => env('CACHE_STORE', 'database'),
確認環境のスケルトンでは limiter キーは未設定(null)でした。ドキュメントでは、レート制限だけ別ストア(例: redis)を指す limiter キーも紹介されています。ローカル確認では array や file、本番の複数サーバでは redis や database を検討するのが一般的です。キャッシュの設定自体はキャッシュの記事側で扱います。
名前付きリミッターを定義する
HTTP / ルート向けには、RateLimiter::for('名前', ...) で名前付きリミッターを定義します。通常は App\Providers\AppServiceProvider::boot に書きます。Limit には perSecond / perMinute / perMinutes / perHour / perDay / none などがあり、->by($key) でユーザー ID や IP ごとに分けられます。
確認環境の app/Providers/AppServiceProvider.php 全体は次のとおりです(use 行は重複させず、この内容で揃えています)。
<?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 register(): void
{
//
}
public function boot(): void
{
RateLimiter::for('uploads', function (Request $request) {
return Limit::perMinute(2)->by($request->ip());
});
RateLimiter::for('verify-api', function (Request $request) {
return Limit::perMinute(3)->by($request->ip());
});
}
}
ここでは IP 単位で、uploads を毎分 2 回、verify-api を毎分 3 回に制限しています。
ルートに throttle ミドルウェアを付ける
名前付きリミッターは、throttle:名前 ミドルウェアでルートへ付けます。制限を超えると HTTP 429 Too Many Requests になります。確認では、429 応答に Retry-After / X-RateLimit-Limit / X-RateLimit-Remaining ヘッダが含まれていました。
routes/web.php 全体(確認環境)は次のとおりです。既定の / に加え、検証用ルートを追加しています。
<?php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::get('/verify-open', function () {
return 'open-ok';
});
Route::middleware(['throttle:verify-api'])->get('/verify-api', function () {
return 'api-ok';
});
Route::middleware(['throttle:uploads'])->get('/verify-upload', function () {
return 'upload-ok';
});
/verify-api(3 回/分)は 3 回まで 200、4 回目から 429(X-RateLimit-Limit=3、Remaining=0、Retry-After=60)でした。/verify-upload(2 回/分)は 2 回まで 200、3 回目が 429 でした。/verify-open には throttle が無いため、連続アクセスでも 200 のままです。
throttle は組み込みのミドルウェアエイリアスとしても登録されています。エイリアス全般の説明は【PHP】Laravelでミドルウェアを実装する方法を参照し、この記事では RateLimiter::for による設定に焦点を当てます。
レスポンス内容に応じてカウントする Limit::after や、Redis 向けの throttleWithRedis については、公式ドキュメントを参照してください。
Feature テスト
Feature テストでは、キャッシュとセッションを array にしたうえで、RateLimiter のヘルパと HTTP 429 を断言できます。確認した tests/Feature/RateLimitTest.php は次のとおりです(3 テスト、12 アサーションが成功)。
<?php
namespace Tests\Feature;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\RateLimiter;
use Tests\TestCase;
class RateLimitTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
Config::set('cache.default', 'array');
Config::set('session.driver', 'array');
Cache::flush();
}
public function test_rate_limiter_attempt_helper(): void
{
$key = 'test-message:42';
RateLimiter::clear($key);
$this->assertSame(5, RateLimiter::remaining($key, 5));
$this->assertTrue((bool) RateLimiter::attempt($key, 5, fn () => true));
$this->assertSame(4, RateLimiter::remaining($key, 5));
}
public function test_throttle_middleware_returns_429_after_limit(): void
{
$this->get('/verify-upload')->assertOk()->assertSee('upload-ok');
$this->get('/verify-upload')->assertOk();
$this->get('/verify-upload')->assertStatus(429);
}
public function test_open_route_not_throttled(): void
{
foreach (range(1, 5) as $i) {
$this->get('/verify-open')->assertOk();
}
}
}
よくあるつまずき
- キャッシュが共有されていない … 複数サーバでは、レート制限用ストアがサーバ間で共有されている必要があります。
- 例外報告の throttle と混同する …
$exceptions->throttleはログ報告の間引きであり、HTTP のthrottle:nameとは別です。 - 確認用リミッターを本番ルートに残す …
/verify-*は検証用です。本番では用途に合った名前と回数に置き換えてください。
まとめ
RateLimiter::attemptで試行を数え、尽きるとfalseになります(decay 既定 60 秒)。- HTTP では
RateLimiter::forとLimit::perMinute(...)->by(...)で名前付きリミッターを定義し、throttle:nameでルートに付けます。 - 制限超過時は 429 とレート制限ヘッダが返ります。
- レート制限はキャッシュに依存します。ローカル確認では
arrayが扱いやすいです。
関連記事
- 【PHP】Laravelでキャッシュを実装する方法
- 【PHP】Laravelでミドルウェアを実装する方法
- 【PHP】LaravelでRESTful APIを作成する方法
- 【PHP】Laravel SanctumでAPIトークン認証を実装する方法
- 【PHP】Laravelでエラー処理(例外の報告・描画とカスタムエラーページ)を実装する方法
- 【PHP】Laravel入門:インストールから簡単なサンプルアプリケーションの作成