Laravelで利用量制限を設計する:レート制限とクォータの使い分け

Webアプリの短時間制限と期間内の利用枠を二つのゲートで表した概念イメージ

API呼び出し、PDF出力、画像処理、ストレージ容量などに上限を設けるとき、単一のカウンターですべてを処理すると運用上の矛盾が生じます。短時間に集中するリクエストを抑える仕組みと、契約期間内の利用量を数える仕組みでは、守る対象も障害時の扱いも異なるからです。

Laravelには時間窓内の試行回数を制御するレート制限が標準で用意されています。一方、日次や月次の利用枠を扱うには、期間境界、保存先、同時実行、失敗時の返却、監査履歴まで含むクォータ設計が必要です。本稿では、この二つを分けて実装するための判断基準を示します。

レート制限とクォータが守るもの

レート制限は、一定の時間窓に許可する試行回数を制限する仕組みです。ログイン試行やAPIへの連続アクセスを抑え、サーバー負荷と不正利用の集中を防ぎます。Laravelの標準機能はキャッシュと組み合わせて動作し、キーごとに残り回数や再試行までの時間を扱えます。

クォータは、利用者や契約ごとに割り当てた総量を期間内で消費する仕組みです。「1分に10回」ではなく、「1か月にPDFを50件」「1日に外部APIを1000回」のような利用枠を管理します。カレンダーに沿った期間と残量表示が必要になる点が、短い時間窓を扱うレート制限との違いです。

比較項目 レート制限 クォータ
主な目的 急激な集中と乱用を抑える 契約上または業務上の利用枠を守る
典型的な単位 秒、分、時間 日、月、請求期間、任意期間
利用者への表示 再試行できる時刻 使用量、残量、期間終了時刻
保存先 高速なキャッシュが中心 監査要件に応じてデータベースまたはキャッシュ
失敗時の論点 試行を数えるか 予約した利用枠を確定するか返却するか

Laravel公式ドキュメントでは、レート制限を「指定した時間窓内のアクションを制限する仕組み」と説明しています。最近紹介されたコミュニティパッケージ「Laravel Quota」は、カレンダー期間ごとの累積利用量を扱い、レート制限とは異なる問題を対象にしています。後者はLaravel本体の標準機能ではないため、採用時には保守状況と実装内容の確認が欠かせません。

実装前に決める六つの条件

誰の利用量を数えるか

キーをユーザーIDだけにすると、組織単位の契約や共有枠を表現できません。利用者、組織、契約、APIキー、機能名を組み合わせ、同じ人物が複数の契約に所属する場合も区別できる識別子を設計します。

何を一単位とするか

リクエスト一回と処理一件は同じとは限りません。一回の操作でPDFを20件生成するなら、消費量は一回ではなく20件かもしれません。外部サービスの費用が処理時間やデータ量で変わる場合は、固定カウントよりもポイント制の方が実態に合います。

いつ期間を切り替えるか

「月次」の開始時刻をサーバーのタイムゾーン任せにすると、利用者の表示と実際のリセットがずれます。契約タイムゾーン、開始日、夏時間、月末の扱いを仕様として固定し、画面とAPIには期間終了時刻を絶対時刻で返します。

どの時点で消費を確定するか

受付時に消費すると、入力エラーや内部障害でも利用枠が減ります。成功時だけ数えるなら、処理中に上限を超える同時リクエストを防ぐ予約が必要です。高コスト処理では「予約、実行、確定または返却」の状態を持たせると、失敗時の動作を説明しやすくなります。

どのデータを正とするか

表示用の残量ならキャッシュで十分な場合があります。しかし、請求金額や顧客との契約に影響する利用量は、キャッシュ消去で失われる設計にできません。変更履歴を残す利用イベントをデータベースへ保存し、集計値を高速な読み取り用データとして扱う構成が安全です。

再送と同時実行をどう扱うか

利用者の二重クリック、通信再送、キュージョブの再実行は、同じ処理を複数回数える原因になります。操作ごとの冪等性キーを保存し、同じキーの消費を一度だけ受け付けます。同じ利用枠を更新する処理は、原子的な加算、データベースのロック、Laravelのキャッシュロックなどで直列化します。

三層に分けると責任が明確になる

実務では、入口のレート制限、期間内のクォータ、監査用の利用台帳を別の層として扱うと設計が安定します。

  1. 入口のレート制限:IPアドレス、利用者、APIキーごとに短時間の集中を抑えます。
  2. 機能別クォータ:契約と機能ごとに残量を確認し、処理に必要な量を予約します。
  3. 利用台帳:予約、確定、返却、管理者による調整を時系列で記録します。

この分離により、攻撃的な連続アクセスは入口で止め、正当な利用者の月次上限は契約ルールとして処理できます。請求や問い合わせが発生した場合も、現在のカウンターだけでなく、いつ何が消費されたかを台帳から説明できます。

Laravel標準のレート制限は、Redis、Memcached、データベースのキャッシュストアで増分値を原子的に扱える方法を提供しています。複数処理をまたぐ排他制御には、Laravelの原子的ロックも使えます。ただし、ロックを導入しただけで二重計上がなくなるわけではありません。処理の再送を識別する冪等性キーと、失敗後に予約を返却する状態遷移も必要です。

保存先は利用量の重要度で選ぶ

用途 適した保存先 注意点
ログイン試行や短時間のAPI制御 Redisなどのキャッシュ 期限設定と障害時の挙動を決める
無料機能の目安となる日次上限 キャッシュまたはデータベース 消失時に上限が緩むリスクを許容できるか確認する
有料プランの月次利用枠 データベース中心 変更履歴と管理者調整を残す
従量課金の請求根拠 追記型の利用イベント 冪等性、監査、再集計の手順を用意する

キャッシュとデータベースの二者択一にする必要はありません。データベースの利用台帳を正とし、現在の残量をキャッシュする構成なら、表示速度と監査性を両立できます。キャッシュ再構築の手順まで運用設計に含めます。

導入を小さく始める手順

  1. 一つの高コスト機能を選び、利用者、単位、上限、期間を文章で定義します。
  2. 入口に短時間のレート制限を置き、クォータとは別のエラーコードを返します。
  3. 利用イベントに冪等性キーを付け、同じ操作の再送を一度だけ計上します。
  4. 残量の確認と消費を一つの原子的な処理にまとめます。
  5. 失敗時の返却と管理者による調整を台帳へ記録します。
  6. 残量、期間終了時刻、制限理由をAPIと画面で同じ表現にします。

既存のLaravelプロジェクトで認証、キュー、テスト、運用まで含む前提を整理したい場合は、PHPとLaravelによる業務Webアプリの設計ガイドも参照できます。データ構造を追加するときは、Laravelのマイグレーション設計に沿ってロールバックと初期データも確認します。

境界条件を中心にテストする

クォータの不具合は通常時より境界で起きます。次のケースを自動テストへ入れます。

  • 上限の直前、上限ちょうど、上限超過
  • 同じ利用枠への同時リクエスト
  • 同じ冪等性キーによる再送
  • 処理成功、入力エラー、内部障害、タイムアウト
  • 日次または月次の切り替わり
  • 契約途中のプラン変更と管理者調整
  • キャッシュ消去後の再構築

LaravelはPestとPHPUnitを標準的なテスト手段として利用でき、機能テストではHTTPリクエストを含む一連の動作を確認できます。実装例を広げるときは、Laravelのテスト戦略にある機能テストとデータベーステストの分担が役立ちます。

コミュニティパッケージを採用する前の確認

Laravel Quotaのようなパッケージは、期間計算、ミドルウェア、保存ドライバーを短時間で導入できる可能性があります。一方、業務上の責任はパッケージへ移りません。採用前に、対応するPHPとLaravelの範囲、ライセンス、更新頻度、テスト、データ移行、ロックの方式、タイムゾーン、失敗時の計上、削除時の復元方法を確認します。

Laravel 13はPHP 8.3以上を必要とします。パッケージ単体の要件がPHP 8.2以上でも、Laravel 13のアプリケーションではフレームワーク側の要件が優先されます。互換性はパッケージの説明だけでなく、実際のComposer制約と対象プロジェクトのロックファイルで確かめます。

よくある質問

月次上限もLaravelのレート制限だけで実装できますか

長い時間窓を設定して数えることはできますが、契約期間、残量表示、調整履歴、失敗時の返却を扱うには責任が増えます。月次上限が契約や請求に関係するなら、短時間のレート制限とは別のクォータと利用台帳を設ける方が仕様を説明しやすくなります。

上限超過時はHTTP 429を返せば十分ですか

ステータスコードだけでは、短時間の集中制限か契約上限かを利用者が区別できません。機械判定できるエラーコード、残量、再開時刻または期間終了時刻を返し、画面にも同じ理由を表示します。

キャッシュだけで有料プランの利用量を管理できますか

キャッシュ消去や障害で値が失われたときに請求根拠を復元できないため、慎重な判断が必要です。有料枠ではデータベースの利用イベントを正とし、キャッシュを高速化に使う構成が適しています。

参考資料

投稿者 greeden Inc.

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)