TL

コンテンツネゴシエーションとVaryによるキャッシュ分割

同じURLで言語や形式を出し分けても、キャッシュ汚染を防げる仕組み。Accept系ヘッダによる表現選択、Varyがキャッシュキーを分割する原理、Client Hintsへの移行と落とし穴まで原理から理解できる。

応用HTTPキャッシュVaryClient HintsCDN最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.コンテンツネゴシエーションは、同一URLに対しクライアントのAccept系ヘッダとサーバーの判断で複数の表現(言語・形式・符号化)から1つを選んで返す仕組み。
  • 2.Varyは「どのリクエストヘッダが応答の選択に影響したか」をキャッシュへ伝え、その値を実効キャッシュキーへ織り込ませることで、誤った表現の配信を防ぐ。
  • 3.Vary対象が多いほどキャッシュは細分化されヒット率が落ちるため、User-Agentでの分岐は避け、Client Hints+Accept-CHへ移行して必要な軸だけを宣言的に絞るのが定石。

コンテンツネゴシエーションとは何か

**コンテンツネゴシエーション(content negotiation)**は、1つのURLが指す1つのリソースに対し、**複数の表現(representation)**を用意しておき、クライアントとサーバーの合意で最適な1つを選んで返す仕組みです。表現が分かれる軸は主に3つあります。言語(日本語版・英語版)、メディアタイプ(text/htmlapplication/json)、コンテンツ符号化(Brotli圧縮版・無圧縮版)です。同じ https://example.com/article でも、受け手の事情に応じて中身を出し分けられます。

ここで核心になるのが、URLは同じまま中身が変わるという点です。URLが違えばキャッシュも別エントリになるので問題は起きません。やっかいなのは「同一URLで複数の答えがある」状態で、これをキャッシュへ正しく教えないと、英語版を要求した利用者へ日本語版が返る、といった**表現の取り違え(キャッシュ汚染)**が発生します。これを防ぐのが後述のVaryです。

ネゴシエーションには2方式あります。サーバーが選ぶサーバー駆動型(Accept系ヘッダを見てサーバーが決定)と、サーバーが選択肢の一覧(300 Multiple Choices)を返してクライアントが選ぶエージェント駆動型です。実運用のほぼ全てはサーバー駆動型なので、本記事もこれを中心に扱います。

Accept系ヘッダによる表現選択

クライアントは、自分が受理できる表現の好みをリクエストヘッダで申告します。代表的なのが4つのAccept系です。

GET /article HTTP/1.1
Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8
Accept-Language: ja, en-US;q=0.8, en;q=0.5
Accept-Encoding: br, gzip
Accept-Charset: utf-8

末尾の;q=は**品質値(q値、0〜1)**で、各候補への相対的な好みを表します。省略時はq=1です。サーバーは「自分が生成できる表現の集合」と「クライアントが受理可能な集合」を突き合わせ、q値の高いものを優先して1つを選びます。Accept-Language: ja, en-US;q=0.8なら、日本語版があれば日本語、無ければ米国英語、という優先順になります。

選択アルゴリズムは概ね次の擬似コードです。

candidates = サーバーが提供できる表現の一覧
for each ヘッダ軸 in [メディアタイプ, 言語, 符号化]:
    クライアントの受理集合とのマッチングでスコア付け(q値を反映)
最終スコアが最大の表現を選択
選択に使った軸を Vary に列挙して応答

重要なのは、サーバーが選択に使った軸を、後で必ずVaryに申告する責任を負う点です。Accept-Languageを見て言語を切り替えたなら、応答にVary: Accept-Languageを付けなければなりません。これを怠ると、キャッシュは言語差を区別できなくなります。

q値は「同点を崩す優先度」であって絶対指定ではない

q値はあくまで相対的な好みです。Accept-Language: en;q=0.9, ja;q=0.1であっても、サーバーが英語版を持たず日本語版しか無ければ日本語が返ります。クライアントは「出せるなら英語が望ましい」と伝えているだけで、存在しない表現を強制はできません。q値を「言語の強制スイッチ」と誤解しないことが要点です。

Varyがキャッシュキーを分割する仕組み

キャッシュ(ブラウザ、CDN、リバースプロキシ)は通常、リクエストのメソッドとURLを主キーとして応答を格納します。しかしコンテンツネゴシエーションが絡むと、同じURLでも返すべき中身がリクエストヘッダによって変わります。そこでVaryの出番です。

Vary応答ヘッダは「この応答の選択に影響したリクエストヘッダ名」を列挙します。キャッシュはこれを読み、実効キャッシュキーを次のように拡張します。

キャッシュキー = (メソッド, URL)                         ← Vary なし
キャッシュキー = (メソッド, URL, Vary に挙げた各リクエストヘッダの値)  ← Vary あり

たとえば応答にVary: Accept-Languageがあれば、キャッシュは「URL+Accept-Languageの値」の組み合わせごとに別エントリを保持します。Accept-Language: jaで来た要求には日本語版エントリを、Accept-Language: enには英語版エントリを返し、両者が衝突しません。

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Language: ja
Vary: Accept-Language, Accept-Encoding

この応答に対し、キャッシュは「URL + 要求のAccept-Language + 要求のAccept-Encoding」をキーにします。言語が2種・符号化が2種なら、最大で 2 × 2 = 4 通りのエントリに分割されます。ここにVaryの本質的なトレードオフがあります。Vary対象の軸が増えるほど、キャッシュは細かく分割され、同じ表現を再利用できる確率(ヒット率)が下がります。

Vary: User-Agent はキャッシュを壊滅させる

Vary: User-Agentを付けると、User-Agent文字列が少しでも違えば別エントリになります。実世界のUser-Agentはバージョン・OS・端末の組み合わせで事実上無限のバリエーションがあり、キャッシュキーが爆発してヒット率がほぼゼロに落ちます。端末別に出し分けたい場合でもVary: User-Agentは避け、後述のClient Hintsで必要な軸だけを宣言するのが正解です。

Varyの落とし穴と「*」

Varyにはもう1つ特別な値があります。Vary: *です。これは「この応答は何らかの未宣言の要因で変わり得るため、いかなるリクエストにも再利用してはならない」という意味で、実質的にキャッシュを無効化します。CDNやプロキシはVary: *を見たら、その応答を共有キャッシュとして保存・再利用しません。意図せず付与すると配信が全てオリジンに戻り、性能を大きく損ないます。

もう1つの典型的な事故が、符号化のVary漏れです。Content-Encodingで圧縮を有効にしながらVary: Accept-Encodingを付け忘れると、CDNは圧縮版と無圧縮版を同一キーで扱い、Brotli非対応のクライアントへBrotli応答を返してしまいます。圧縮の交渉とVaryの関係はBrotli/gzipによるコンテンツ符号化と転送圧縮の原理で詳述しています。

Vary の値意味キャッシュへの影響
指定なしURL とメソッドだけでキャッシュヒット率は最大。ただし表現の取り違えリスク
Accept-Encoding圧縮方式ごとに分割圧縮版/無圧縮版を正しく区別(実質必須)
Accept-Language言語ごとに分割言語数ぶんエントリ増。Cookieでの切替なら別途検討
User-AgentUA文字列ごとに分割バリエーション爆発でヒット率が壊滅(非推奨)
*未宣言要因で変動事実上キャッシュ不可(共有キャッシュは保存しない)

なお、キャッシュ側のVaryの扱いは「正規化(normalization)」の有無で結果が変わります。素朴な実装はAccept-Encodingの値を文字列としてそのまま比較するため、gzip, brbr, gzipを別物と見なしエントリが無駄に増えます。CDNの多くはAccept-Encodingbrgzipidentityの少数の正規形へ畳んでからキーにし、分割数を抑えています。自前のキャッシュ層を実装するなら、この正規化を入れるかどうかでヒット率が大きく変わります。

Client Hintsへの移行と注意点

User-Agentを端末判定や画像出し分けに使う慣習は、上記のとおりキャッシュと相性が最悪です。これを置き換えるのがClient Hints(クライアントヒント)です。サーバーが「欲しい情報の種類」を宣言し、ブラウザがそれに応じて必要な軸だけを専用ヘッダで送る、という協調モデルになっています。

仕組みは2段階です。まずサーバーが応答でAccept-CHを送り、受け取りたいヒントを宣言します。

HTTP/1.1 200 OK
Accept-CH: Sec-CH-DPR, Sec-CH-Viewport-Width, Sec-CH-Width
Vary: Sec-CH-DPR, Sec-CH-Viewport-Width

次回以降、ブラウザは宣言されたヒントだけをリクエストに付けます(例: Sec-CH-DPR: 2 でデバイスピクセル比、Sec-CH-Viewport-Width: 1280 でビューポート幅)。サーバーはこれを見て、たとえば高解像度端末には2倍密度の画像を返す、といった出し分けができます。出し分けに使ったヒントは、やはりVaryに列挙してキャッシュへ伝えます。

この方式の利点は、キャッシュ分割の軸を粗い離散値に保てることです。User-Agentの無限のバリエーションと違い、DPRは実質1・2・3程度、ビューポート幅もサーバー側でブレークポイント単位に丸めれば数通りに収まります。必要な軸だけをVaryに入れられるので、ヒット率を保ったまま端末適応ができます。

Client Hints は初回リクエストには付かない

ブラウザがヒントを送るのは、サーバーがAccept-CHで宣言した後の2回目以降です。初回リクエストにはヒントが付きません。そのため初回は控えめなデフォルト(標準解像度の画像など)で応答し、Accept-CHを返して2回目以降に最適化する設計が必要です。初回からヒント前提のロジックを書くと、ヒント欠落時に破綻します。

Critical-CH で初回往復のロスを縮める

Accept-CHに加えてCritical-CHを返すと、ブラウザは「そのヒントが無いと正しく応答できない」と理解し、ヒントを付けてその場でリクエストを自動的に再送します。初回の最適化漏れを1往復で回収でき、重要なヒント(DPRなど)を確実に効かせたい場合に有効です。ただし再送が増えるので、本当に必須の軸だけをCritical-CHに入れます。

試験・設計レビューの要点

「同一URLで中身が変わるのにVaryが無い」構成は誤りです。逆に「全応答にVary: User-Agent」も誤りで、キャッシュ無効化に等しい。正解は『選択に実際に使った軸だけVaryに列挙し、UA依存はClient Hintsへ移す』。Vary: *はキャッシュ不可の宣言である点も頻出ポイントです。

まとめ

コンテンツネゴシエーションは、同一URLで言語・形式・符号化などの表現を出し分ける仕組みで、サーバーはAccept系ヘッダとq値を突き合わせて1つを選びます。その選択をキャッシュへ正しく伝える要がVaryで、Varyに挙げた軸は実効キャッシュキーへ織り込まれ、表現の取り違えを防ぎます。代償としてVary対象が増えるほどキャッシュは分割されヒット率が落ちるため、User-Agentでの分岐は避け、Client HintsとAccept-CHで必要な軸だけを離散値で宣言するのが現代の定石です。キャッシュキー分割の全体像はブラウザキャッシュの階層と判定アルゴリズムを、鮮度と再検証の基礎はHTTP キャッシュを、ヘッダ自体を圧縮して送る仕組みはHTTP/2の多重化とHPACKヘッダ圧縮の原理を併読すると、配信最適化の判断軸がつながります。

Web/フロントエンド Article

コンテンツネゴシエーションとVaryによるキャッシュ分割を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

HTTP

比較で見る軸

難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5

導入後に効く点

Varyは「どのリクエストヘッダが応答の選択に影響したか」をキャッシュへ伝え、その値を実効キャッシュキーへ織り込ませることで、誤った表現の配信を防ぐ。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
Web/フロントエンド
タグ数
5

判断チェックリスト

  • 自社の用途が「HTTP / キャッシュ」に近いか確認する。
  • 強みである「コンテンツネゴシエーションは、同一URLに対しクライアントのAccept系ヘッダとサーバーの判断で複数の表現(言語・形式・符号化)から1つを選んで返す仕組み。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

HTTPキャッシュVaryClient HintsCDNHTTPキャッシュVary
参考: 公式情報