レート制限パターン

API やサービスへのリクエスト数を制限し、過負荷やリソース枯渇を防ぐ設計パターン

API設計パターン

レート制限パターンとは

レート制限 (Rate Limiting) は、API やサービスへのリクエスト数に上限を設け、過負荷やリソース枯渇を防ぐ設計パターンである。DDoS 攻撃の緩和、公平なリソース配分、コスト制御に使われる。

主要なアルゴリズム

アルゴリズムの違いは「何を数えるか」と「バーストをどこまで許すか」に現れる。固定ウィンドウとスライディングウィンドウはリクエストの件数を数え、トークンバケットとリーキーバケットは通過させる速度そのものを制御する。

アルゴリズム特徴用途
固定ウィンドウ時間窓ごとにカウントシンプルな API 制限
スライディングウィンドウ直近 N 秒のカウントより正確な制限
トークンバケットトークンを一定速度で補充バースト許容
リーキーバケット一定速度でリクエストを処理平滑化

固定ウィンドウは実装が単純だが、窓の切り替わりをまたぐ集中に弱い。1 分あたり 100 回という設定なら、59 秒目に 100 回・60 秒目に 100 回を通してしまい、2 秒間では上限の 2 倍が流れる。スライディングウィンドウはこの穴を塞げるが、リクエストごとの時刻を保持する必要があり、保存するデータ量と参照回数が増える。トークンバケットは平常時の速度 (補充速度) とバーストの許容量 (バケット容量) を別々に指定できるため、この 2 つを分けて設計したい API に向く。

API Gateway はトークンバケットアルゴリズムを使用する。使用量プランやステージの設定では、ThrottlingRateLimit がトークンの補充速度 (1 秒あたりのリクエスト数)、ThrottlingBurstLimit がバケットの容量に対応する。

ここで注意したいのは、これらが厳密な天井ではないことである。開発者ガイドはスロットリングとクォータを、可能な範囲で適用されるものであり、保証された上限ではなく目標値と考えるべきものと明記している。バーストによる超過が設計上許されているほか、他の要因でも設定値を超えて通り得る。

設定は 4 段階あり、①使用量プランで設定したクライアント別・メソッド別の制限 ②ステージのメソッド別の制限 ③アカウント単位 (リージョンごと) の制限 ④ AWS 側のリージョン制限 の順に適用される。③はアカウント内の全 API 合計に効き、東京リージョンの既定値は定常 10,000 リクエスト/秒・バースト 5,000 である (2026 年 8 月時点)。つまり 1 つの API が出したバーストは、同じアカウントの別の API が使える枠を削る。上限を超えたクライアントには 429 Too Many Requests が返る。

DynamoDB でのレート制限

カウンターを複数のサーバーやコンテナで共有する場合、値を読んでから足して書き戻す作りにすると、同時に届いたリクエストどうしでカウントを取りこぼす。加算をデータストア側の更新式に任せれば、1 回のリクエストで加算と加算後の値の取得が完結するため、この取りこぼしが起きない。窓の切り替えは、キーに時刻を丸めた値を含めることで表現する。

import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, UpdateCommand } from '@aws-sdk/lib-dynamodb';

const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({}));

async function checkRateLimit(userId: string, limit: number): Promise<boolean> {
  const now = Math.floor(Date.now() / 1000);
  const windowKey = `${userId}:${Math.floor(now / 60)}`; // 1 分ウィンドウ

  const result = await ddb.send(
    new UpdateCommand({
      TableName: 'rate-limits',
      Key: { pk: windowKey },
      UpdateExpression: 'ADD #count :inc SET #ttl = :ttl',
      ExpressionAttributeNames: { '#count': 'count', '#ttl': 'ttl' },
      ExpressionAttributeValues: { ':inc': 1, ':ttl': now + 120 },
      ReturnValues: 'ALL_NEW',
    })
  );

  return Number(result.Attributes?.count ?? 0) <= limit;
}

countttl はどちらも DynamoDB の予約語なので、ExpressionAttributeNames で別名を与えないと式が拒否される。ttl 属性はテーブルで TTL を有効化して初めて意味を持ち、数値型の Unix 秒でない値は TTL の処理から無視される。

TTL による削除は期限の到来と同時ではなく、期限切れから数日以内に行われる仕様である。したがって TTL は古いカウンターの片付け役であって、窓の切り替えを担っているわけではない。窓が変わればキーも変わるので、削除がいつ走るかは制限の正しさに影響しない。

コストの見方も押さえておきたい。この方式では通過・拒否のどちらであってもリクエスト 1 件ごとに書き込みが 1 回発生するため、レート制限そのものが書き込み容量を消費する。上限に達したクライアントが叩き続ける状況では、拒否する分の書き込みも積み上がる。

レスポンスヘッダー

クライアントは自分がどれだけ枠を使ったかを知らされなければ、上限に当たるまで送り続けるしかない。残り回数と回復の時刻を応答に載せておくと、呼び出し側が間隔を空ける判断ができる。

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1711929600

HTTP/1.1 429 Too Many Requests
Retry-After: 30

X-RateLimit- で始まるヘッダーは HTTP の規格として定義されたものではなく、実装ごとの慣行である。名前も値の意味も統一されておらず、X-RateLimit-Reset に「枠が回復するまでの残り秒数」を入れる実装と、上の例のように Unix 秒の時刻を入れる実装がある。どちらなのかを API のドキュメントに明記しないと、秒数と時刻を取り違えたクライアントの待ち時間が何十年もずれる。

429 に添える Retry-After は秒数でも日時でも書けるが、全クライアントが指定どおりの時刻に再送すると、待機が明けた瞬間に再び集中する。呼び出し側では指定値を下限として、乱数の揺らぎを足してから再送するのが安全である。

多層防御

1 つの層だけで守ろうとすると、粒度と費用のどちらかで無理が出る。エッジで弾けば計算資源を使う前に切り離せるが、どのユーザーの枠かという判断はできない。逆にアプリケーション層はユーザー単位で正確に数えられるが、そこまで到達させると 1 リクエストごとに計算とデータストアの費用がかかる。そこで粗い網から細かい網へと順に置く。

CloudFront (WAF) → API Gateway (スロットリング) → Lambda → DynamoDB
  ↑ IP ベース制限    ↑ API キーベース制限      ↑ ユーザーベース制限

レイヤーごとに異なる粒度でレート制限を適用する。

最前段の AWS WAF のレート制限 (レートベースルール) は、集計単位ごとに直近一定時間のリクエスト数を数える仕組みである。評価期間は 60・120・300・600 秒から選び、既定は 300 秒。設定できる上限値の下限は 10 リクエストで、それより細かい制限はここでは表現できない。上限は集計単位 (送信元 IP アドレスやヘッダーの値など、選んだキーごとのまとまり) それぞれに適用される。

ただし WAF の数え方は設定値との厳密な一致を保証しない。開発者ガイドは、直近のリクエストを重く見る推定を使うため設定値の近くで制限がかかること、評価期間や伝播の遅れにより検知までに数分かかる場合があること (通常は 30 秒未満)、設定を変更するとカウントがリセットされて最大 1 分ほど制限が止まることを明記しており、精密なレート制限を意図した機能ではないと述べている。可用性を守る大量アクセス対策は WAF に、「1 ユーザーあたり毎分 100 回」のような約束を正確に守らせる役目はアプリケーション層に置くのが妥当である。

レート制限パターンの理解を深めるには関連書籍が参考になる。

この記事は役に立ちましたか?

関連用語

関連する記事