Envoy Gateway 1.3.0 – 新機能「Rate Limiting with Cost」の概要

## BackendTrafficPolicy CRD における Cost Specifier

Envoy Gateway v1.3.0では、**BackendTrafficPolicy**カスタムリソースに定義するレートリミット規則（RateLimitRule）に**コスト指定子（Cost specifier）**が追加されました。各レートリミットルールにオプションで`cost`フィールドを設定でき、リクエストおよびレスポンスそれぞれに対して「コスト（消費量）」を指定できます。デフォルトでは`cost`を指定しない場合、**リクエストごとに1カウント**を消費し、レスポンス時には何も消費しません。  

`cost`フィールドは以下の2つのサブフィールドから構成されます：

- **`request`**: リクエスト受信時に消費するコストを指定します。例えば`request`コストを「5」に設定すれば、そのルールにマッチしたリクエストは**一度に5カウント**消費します。指定しない場合はデフォルトで1を消費します。Envoyがリクエストを受信した際、ここで指定された数だけレートリミットカウンタを減算し、**残高が不足していればそのリクエストを制限（ブロック）**します。

- **`response`**: レスポンス送信後（ストリーム終了時）に追加で消費するコストを指定します。例えば`response`コストを「10」とすれば、該当リクエストのレスポンス完了後に**さらに10カウント**消費します。レスポンス時のコストは**現在処理中のリクエストには適用されず**、そのリクエストは一旦通過しますが、**その後カウンタが減算されることで次回以降のリクエストに影響**します。`response`を指定しなければ、レスポンス時の減算は行われません（デフォルト動作）。この`response`コスト指定は現在**グローバルレートリミット（全Envoy間で共有される制限）**の場合にのみサポートされ、ローカルレートリミットには適用できません。

## Cost Specifierの詳細仕様

コストの値指定には**固定値（Number）**と**動的メタデータ（Metadata）**の2種類がサポートされています。各`request`/`response`サブフィールドは`from`でソースを指定し、以下のように設定します：

- **`from: "Number"`** – 固定の数値を指定します。この場合`number`フィールドに正の整数値を設定し、その値がコストとして使用されます。例えば`request.from: "Number", request.number: 3`とすれば、リクエスト時に毎回3カウント消費します。**特別なケース**として、`number: 0`と設定すると**カウンタのチェックのみを行い減算しない**動作になります。これは後述のように「予算が残っているか確認するが実際には消費しない」用途で利用できます。

- **`from: "Metadata"`** – Envoyの**動的メタデータ**から値を取得してコストとします。この場合、`metadata`フィールドで`namespace`（メタデータの名前空間）と`key`（キー）を指定します。Envoyフィルタチェーン中の他のフィルタ（Luaスクリプト、External Processingフィルタ等）がリクエストまたはレスポンス処理中に動的メタデータに数値をセットしておき、レートリミットフィルタがそれを参照してコストとして使用します。例えば`response.from: "Metadata", response.metadata: {namespace: "extproc", key: "token_usage"}`のように設定すれば、External Processingフィルタ等がレスポンスボディ解析で設定した`extproc`名前空間の`token_usage`というメタデータ値を、レスポンス完了時のコストとして消費できます。

## 「Rate Limiting with Cost」の動作原理

**Rate Limiting with Cost**機能は、Envoy本体の新拡張機能を活用して実現されています。Envoyのグローバルレートリミットフィルタに、**リクエスト完了後にカウントを減算する機能**（`apply_on_stream_done` オプション）と、**ヒット数加算（hits_addend）の動的指定**が追加されたことにより、Envoy Gateway側でコスト指定子が扱えるようになりました。具体的な動作は次の通りです:

- リクエスト到着時、Envoy GatewayはBackendTrafficPolicyで定義されたRateLimitルールに基づき、`request`コストをグローバルレートリミットサービスに問い合わせます。`from: Number`の場合は指定値、`from: Metadata`の場合はリクエストに付随するメタデータから取得した値（例えば特定のリクエストヘッダを事前に別フィルタで動的メタデータに写しておく）を**hits_addend**として送信し、レートリミットサービス上のカウンタを減算します。この時点で**上限を超えるようならリクエストは即座に429などで拒否**されます。

- レスポンス送信時（ストリーム終了時）、もし`response`コストが設定されていれば、Envoyは**改めて**その値分のカウント減算処理を行います。`apply_on_stream_done`オプションにより、このレスポンス時の処理は**「実行するだけで現在のリクエスト自体はブロックしない」**（**fire-and-forget**のような動作）と定義されています。したがって、レスポンス時のコストによって**そのリクエスト後の残り予算が減る**形になり、**次のリクエスト以降に制限が反映**されます。特にストリーミングレスポンスなど**レスポンスボディの内容で使用量が決まる場合**（例: OpenAI APIのトークン消費量）、External ProcessingフィルタやLuaフィルタでレスポンスを解析し、**最終行で使用量を動的メタデータに記録**⇒レートリミットフィルタがストリーム完了時にそれを読み取ってカウント減算、という連携が可能です。この方法なら、レスポンス全体を送り終えるまで現在のリクエストを遮ることなく、**終了後に正確なコストだけを課金**できます。

- **予算超過の事前チェック**: レスポンスコストのみで制限を行いたい場合でも、**すでに予算がゼロの場合のリクエストを許可してしまう**と使い放題になってしまいます。これを防ぐため、**リクエスト時に減算しないチェック**として`request`コストに0を指定する使い方が議論されています。`request.number: 0`とすると**「カウンタ残高の照会のみ」**を行い（残がなければブロック）つつ、実際の消費は行わないため、**トークンベースで残高ゼロのときはリクエスト自体を拒否**し、それ以外のケースではレスポンス終了時に実際の使用量を減算する、といった構成が可能です。

この機能の背景には、Generative AI APIなど**リクエストごとにコストが異なるユースケース**への対応があります。Envoy Gatewayの開発者によるIssueでの議論でも、OpenAIやAWSのストリーミングAPIの例を挙げ、**レスポンス終端で使用トークン数を取得して課金する必要**があること、そのため**ヘッダではなく動的メタデータで値を受け渡す**設計が適切であることが指摘されています。この要件に対応するためにEnvoy本体とEnvoy Gateway双方に拡張が施されました（EnvoyのPR *envoyproxy/envoy#37548* 等で対応）。

## 実装コードの変更点（PR #4957 および #5035）

- **PR [#4957](https://github.com/envoyproxy/gateway/pull/4957)** (*“api: adds cost specifier to RateLimitRule”*): Envoy GatewayのAPI定義にコスト指定子を追加したPRです。この変更でBackendTrafficPolicyの`RateLimitRule`に`Cost`フィールド（上記の`request`/`response`等を含む構造体）が導入されました。PRの説明によると、この機能は**Envoy側の「ストリーム終了時に予算を減算する」新機能と「ディスクリプタ毎の可変ヒット数設定（hits_addend）」**を利用して実現されており、レスポンス内容に基づく**「トークンベースのレート制限」**を可能にする目的があります。例えば、AI Gatewayのようにユーザごとにトークン消費量でレート制限を行うケースを想定しています。このPRではKubernetes CRDのスキーマ拡張のみを行い、実際のEnvoy設定生成（translator部分）は含まれていません（実装は次のPRで行われました）。

- **PR [#5035](https://github.com/envoyproxy/gateway/pull/5035)** (*“feat(translator): implement ratelimit costs”*): 上記API拡張を受けて、Envoy Gateway内部での翻訳ロジック（xDS変換）にコスト指定を実際に適用する実装を行ったPRです。この変更により、BackendTrafficPolicyで`cost`が指定された場合にEnvoyのレートリミット設定（RateLimitカスタムHTTPフィルタ）へ適切な設定が挿入されます。具体的には、**`response`コストが指定された場合**にはEnvoyの**新しい typed_per_filter_config（RateLimitPerRoute）**形式での設定を使い、各ディスクリプタに`apply_on_stream_done: true`（ストリーム完了時適用）や`hits_addend`の指定を行う対応が実装されています。これはEnvoy本体の比較的新しいバージョン機能を必要とするため、Envoy Gateway側でも**Envoy v1.33.0以降**を仮定して処理しています（古いEnvoyとの下位互換にも配慮し、`cost`未使用時は従来通りの設定を用いるなどの実装がされています）。また、このPRではエンドツーエンドテストを拡張し、**実際にリクエスト・レスポンスでカウンタが期待通り減算されること**（例えばX-RateLimit-Remainingヘッダの挙動）を検証しています。なお、グローバルレートリミットサービス（envoyproxy/ratelimit）の対応も必要となるため、テストでは最新版が使われています。

## 関連Issueやディスカッション

Rate Limiting with Cost機能の検討経緯はGitHub上のIssueでも議論されています。**Issue [#4756](https://github.com/envoyproxy/gateway/issues/4756)**「Usage based Rate Limiting (Counting from response header values)」では、**レスポンスヘッダの値を用いた使用量ベースのレート制限**というテーマで議論が行われました。この中で、ストリーミングレスポンスの場合に**レスポンス終了まで使用量が確定しない問題**をどう扱うかが議題となり、**動的メタデータ＋ストリーム終了時処理**のアプローチが提案されています。また、このIssueは**Generative AI対応のエピックIssue [#4748](https://github.com/envoyproxy/gateway/issues/4748)**配下のタスクとして扱われ、他にも**ExtProcフィルタでボディから値を抽出してヘッダやメタデータに載せる機能**（Issue #4758）等と合わせて検討されました。最終的に、Issue #4756はPR #5035のマージによりクローズされており、この機能がv1.3.0で実現されたことが分かります。

## 設定方法と使用例

**設定方法**としては、BackendTrafficPolicyリソース内でレートリミットを定義する際に`cost`フィールドを追加する形になります。例えば、**ユーザごとに月間消費トークン数を制限する**ようなケースを想定し、1リクエストあたり**最大1000トークン**使用可能（それ以上は制限）とするには以下のような設定が考えられます（※簡略化した例）:

```yaml
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: BackendTrafficPolicy
metadata:
  name: example-ratelimit
spec:
  targetRef:
    kind: HTTPRoute
    name: my-service-route
  rateLimit:
    type: Global
    global:
      rules:
      - headers:                      # ここで任意のマッチ条件を指定（例: 全リクエスト共通なら省略可）
          - name: X-User-ID           # ユーザIDヘッダごとにレートを分ける例
            present: true
        limit:
          requests: 100              # ベースのリクエスト数上限（100リクエスト/月など）
          unit: Month
        cost:
          request:
            from: Number
            number: 0                # リクエスト時は消費せず予算チェックのみ
          response:
            from: Metadata
            metadata:
              namespace: extproc     # External Processingフィルタが設定するメタデータ例
              key: tokens_used       # レスポンス内で実際に使用したトークン数
```

上記の例では、`request.cost.number`を0にすることで**現在残っているリクエスト枠が0の場合のみブロック**し、そうでなければ一旦通過させます。実際のトークン消費量は、ExtProcフィルタなどがレスポンスを解析して`tokens_used`というメタデータに設定し、それを`response.cost`が読み取って**ストリーム終了時に減算**します。こうすることで、**各リクエストが実際に何トークン消費したかに基づいて**グローバルなレートリミットカウンタを減らすことができます。

なお、固定コストの場合は単純に`from: Number`かつ`number: <値>`を指定するだけで、例えば**「この特定APIエンドポイントは通常リクエスト5件分の重みを持つ」**といったポリシーを実現できます。逆に**コストを動的に変えたい場合**（例えばリクエスト内容やレスポンス内容によって異なる負荷をカウントしたい場合）は`from: Metadata`を使い、別途フィルタでそのメタデータ値を設定することで柔軟なレート制御が可能です。

## まとめ

Envoy Gateway 1.3.0の「Rate Limiting with Cost」機能により、**レート制限の単位を1リクエスト＝1カウントに固定せず**、処理内容に応じた重み付けや使用量連動の制限が可能になりました。公式のリリースノートやブログでも、この機能を「**Dynamic Cost Based Rate Limiting**」と呼び、**動的メタデータから値を取得してコストとすることで、コストの異なるリクエスト間で公平に利用制限を行える**ことが強調されています。実装面ではEnvoy本体およびEnvoy Gatewayの拡張に支えられており、特に**Generative AI等のユースケースで求められるトークンベースのAPIレート制限**に対応するものとなっています。

**参考資料:**

- Envoy Gateway v1.3.0 リリースノート（新機能一覧）
- Envoy Gateway APIリファレンス（BackendTrafficPolicy / RateLimitCost仕様）
- Envoy Gateway PR #4957 説明（Cost Specifier追加の背景と目的）
- Envoy Gateway PR #5035 説明・コメント（実装詳細やテスト）
- 関連Issueの議論（#4756 Usage based RL、#4748 GenAI対応）