本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
RFC 9110 Cache-Control 詳解: HTTPキャッシュ制御の深掘り
背景
Webアプリケーションのパフォーマンスとスケーラビリティにおいて、HTTPキャッシングは不可欠な要素です。クライアントとオリジンサーバー間のネットワークトラフィックを削減し、応答時間を短縮することで、ユーザーエクスペリエンスを向上させます。HTTPキャッシングの挙動を詳細に制御するために、Cache-Controlヘッダフィールドが用いられます。
2022年6月に発行されたRFC 9110「HTTP Semantics」[1] は、HTTP/1.1から継続して使用されてきたHTTPキャッシングのセマンティクスを再定義し、Cache-Controlヘッダの具体的なディレクティブとその効果を網羅的に記述しています。本記事では、RFC 9110に基づいてCache-Controlヘッダの内部動作、主要なディレクティブ、相互運用性、セキュリティ上の考慮事項、および実装上の注意点をネットワークエンジニアの視点から詳解します。
設計目標
Cache-Controlヘッダの主要な設計目標は以下の通りです。
キャッシュ制御の柔軟性: サーバーとクライアントの両方が、リソースのキャッシュ可能性、鮮度、再検証ポリシーをきめ細かく制御できるようにする。
効率的なリソース利用: 不必要なネットワークリクエストを削減し、サーバーの負荷を軽減するとともに、クライアントの応答時間を短縮する。
データの一貫性: キャッシュされたリソースが古くなることを防ぎ、必要に応じてオリジンサーバーとの再検証メカニズムを提供する。
セキュリティとプライバシーの保護: 機密情報が意図せずキャッシュに保存されたり、共有キャッシュに公開されたりすることを防ぐ。
詳細
Cache-Controlヘッダは、リクエストとレスポンスの両方で使用され、キャッシュの動作に影響を与える一連のディレクティブ(指示子)を含みます。
Cache-Control ヘッダの構造
Cache-Controlヘッダは、カンマ区切りのディレクティブのリストとして定義されます。一部のディレクティブは値を取ります。
Cache-Control: directive-name [= value], directive-name [= value], ... 例: Cache-Control: max-age=3600, public Cache-Control: no-cache, no-store, must-revalidate
主要なディレクティブ
RFC 9110で定義されている主なCache-Controlディレクティブを、その種類と目的別に解説します。
1. キャッシュ可能性制御
no-store:目的: リクエスト/レスポンスのいかなる部分もキャッシュに保存してはならないことを指示します。機密情報を含むリソースに必須です。
種類: リクエスト/レスポンス
no-cache:目的: キャッシュされたリソースを使用する前に、オリジンサーバーで再検証しなければならないことを指示します。キャッシュへの保存自体は許可されますが、必ず鮮度確認が必要です。
種類: リクエスト/レスポンス
public:目的: レスポンスを共有キャッシュ(プロキシなど)にキャッシュできることを指示します。通常、認証が必要なリソースはデフォルトでプライベートキャッシュのみに制限されるため、明示的に共有キャッシュでのキャッシュを許可する場合に使用します。
種類: レスポンス
private:目的: レスポンスをプライベートキャッシュ(単一ユーザーのブラウザキャッシュなど)のみにキャッシュできることを指示します。共有キャッシュはこれをキャッシュしてはなりません。
種類: レスポンス
2. 鮮度制御
max-age=<seconds>:目的: キャッシュされたレスポンスが「新鮮」であると見なされる最大秒数を指定します。この期間が過ぎると、レスポンスは「陳腐(stale)」となり、通常は再検証が必要になります。
種類: レスポンス
s-maxage=<seconds>:目的:
max-ageと同様ですが、共有キャッシュ(プロキシなど)にのみ適用されます。プライベートキャッシュには影響しません。max-ageよりもs-maxageが優先されます。種類: レスポンス
immutable:目的: リソースが変更されないことを示し、キャッシュがそのリソースを再検証なしで一定期間(通常1年程度)提供できることを指示します。これにより、クライアントは再検証リクエストを省略できます。
種類: レスポンス
3. 再検証制御
must-revalidate:目的: キャッシュが陳腐化した後、オリジンサーバーとの再検証なしに陳腐化したレスポンスを返してはならないことを指示します。オリジンサーバーが利用できない場合でも、陳腐化したレスポンスの提供は許可されません。
種類: レスポンス
proxy-revalidate:目的:
must-revalidateと同様ですが、共有キャッシュにのみ適用されます。プライベートキャッシュには影響しません。種類: レスポンス
4. その他のディレクティブ
no-transform:目的: キャッシュやプロキシがレスポンスのペイロードを変換してはならないことを指示します(例: 画像形式の最適化、HTMLの圧縮など)。
種類: リクエスト/レスポンス
stale-while-revalidate=<seconds>:目的: キャッシュが陳腐化した後、指定された秒数内であれば、オリジンサーバーへの非同期的な再検証と並行して陳腐化したレスポンスをすぐに提供できることを指示します。
種類: レスポンス
stale-if-error=<seconds>:目的: キャッシュが陳腐化した後、オリジンサーバーへの再検証がエラーになった場合、指定された秒数内であれば陳腐化したレスポンスを提供できることを指示します。
種類: レスポンス
キャッシュのライフサイクルシーケンス
以下に、Cache-Controlディレクティブを考慮したキャッシュの一般的なシーケンス図を示します。
sequenceDiagram
participant Client
participant "Proxy as Shared Cache"
participant "Origin as Origin Server"
Client ->> Proxy: GET /resource A (初回リクエスト)
alt Cache miss or `no-cache` (Request)
Proxy ->> Origin: GET /resource A If-None-Match: "etag"
Origin -->> Proxy: HTTP/200 OK Cache-Control: max-age=3600, public ETag: "new-etag"
Proxy ->> Proxy: Store resource A in cache
Proxy -->> Client: HTTP/200 OK (from Origin)
else Cache hit, fresh (`max-age` not expired)
Proxy -->> Client: HTTP/200 OK (from cache)
else Cache hit, stale but revalidation allowed
Proxy ->> Origin: GET /resource A If-Modified-Since: ... OR If-None-Match: "etag"
Origin -->> Proxy: HTTP/304 Not Modified (Content not changed)
Proxy ->> Proxy: Update freshness lifetime
Proxy -->> Client: HTTP/200 OK (from cache, revalidated)
else Cache hit, stale and `must-revalidate` (Origin unreachable)
Proxy ->> Origin: GET /resource A If-Modified-Since: ... OR If-None-Match: "etag"
Origin --x Proxy: Network Error (Origin unreachable)
Proxy --x Client: HTTP/504 Gateway Timeout (Cannot revalidate)
end
Client ->> Proxy: GET /resource B (後続リクエスト)
alt `stale-while-revalidate` active
Proxy -->> Client: HTTP/200 OK (stale from cache)
Proxy ->> Origin: GET /resource B If-None-Match: "old-etag" (async revalidation)
Origin -->> Proxy: HTTP/200 OK ETag: "new-etag" (async update)
Proxy ->> Proxy: Update resource B in cache
end
キャッシュ判断ロジック(簡略化)
graph TD
A["リクエスト/レスポンス受信"] --> B{"Cache-Controlヘッダは存在するか?"};
B -- No --> C["デフォルトのキャッシュポリシーを適用"];
B -- Yes --> D{"no-storeディレクティブは存在するか?"};
D -- Yes --> E["いかなる場合もキャッシュしない"];
D -- No --> F{"no-cacheディレクティブは存在するか?"};
F -- Yes --> G["キャッシュするが、利用時は常に再検証を要求"];
F -- No --> H{"max-ageまたはs-maxageディレクティブは存在するか?"};
H -- Yes --> I["指定された期間、新鮮と見なしてキャッシュ"];
H -- No --> J["他のヘッダ(Expires, Last-Modified)または heuristically な鮮度決定"];
G --> K["キャッシュ保存の判断"];
I --> K;
J --> K;
K --> L{"public/privateディレクティブは存在するか?"};
L -- public --> M["共有キャッシュに保存可能"];
L -- private --> N["プライベートキャッシュのみに保存可能"];
L -- 該当なし --> N;
M --> O["レスポンスを送信"];
N --> O;
E --> O;
C --> O;
相互運用性
Cache-Controlヘッダは、HTTP/1.1で導入され、HTTP/2およびHTTP/3においてもそのセマンティクスは維持されています。
HTTP/1.1との比較
Cache-ControlはHTTP/1.1のコア機能であり、基本的な動作は変更されていません。HTTP/1.1では、同一のTCP接続上でのリクエストのHOL (Head-of-Line) Blockingが発生する可能性があり、キャッシュの利用は通信効率を向上させる重要な手段でした。
HTTP/2 および HTTP/3 との比較
| 特徴 | HTTP/1.1 (Cache-Control) | HTTP/2 (Cache-Control) | HTTP/3 (Cache-Control) |
|---|---|---|---|
| ベースプロトコル | TCP | TCP + TLS | QUIC + TLS 1.3 |
| ヘッダ圧縮 | なし | HPACK | QPACK (効率的) |
| 多重化 | なし (パイプライン化はHOL Blockingを伴う) | 単一TCP接続上で多重化 (ストリーム) | 単一QUIC接続上で多重化 (ストリーム) |
| HOL Blocking | アプリケーション層で発生 | TCP層では発生する可能性あり (多重化による軽減) | QUICストリームは独立しているため、アプリケーション層でもTCP層でも発生しない |
| 0-RTT | なし | なし | QUICハンドシェイクでサポート (高速接続確立) |
Cache-Controlの適用 |
同一のセマンティクスで適用 | 同一のセマンティクスで適用、ヘッダ圧縮によりオーバーヘッド減 | 同一のセマンティクスで適用、QPACKによりオーバーヘッド減、0-RTTと合わせてキャッシュ戦略が重要 |
| キャッシュの役割 | 非常に重要。パフォーマンスのボトルネックを直接解消。 | 依然重要だが、多重化により一部のボトルネックが解消。 | ネットワーク遅延の低減に加えて、0-RTTからの再送攻撃対策としても重要。 |
HTTP/2およびHTTP/3は、ネットワークレベルでのパフォーマンス改善(多重化、ヘッダ圧縮、0-RTT、HOL Blocking回避)を提供しますが、Cache-Controlによるアプリケーションレベルでのキャッシュ制御の重要性は変わりません。むしろ、これらの高速プロトコルにおいても、不必要なデータ転送を避けるためにCache-Controlの適切な利用が求められます。特にHTTP/3の0-RTTでは、古い状態でのリクエスト送信による問題を防ぐため、キャッシュの鮮度管理がより一層重要になります。
セキュリティ考慮事項
Cache-Controlの不適切な使用は、セキュリティリスクを引き起こす可能性があります。
機密情報の漏洩:
no-storeが指定されていない場合、個人情報や認証情報を含むレスポンスが意図せずキャッシュに保存される可能性があります。特に共有キャッシュ(プロキシ)にこれらの情報がキャッシュされると、他のユーザーに情報が漏洩するリスクが高まります。privateディレクティブが誤ってpublicとして扱われる、または全く指定されない場合、共有キャッシュにプライベートなコンテンツが保存され、他のユーザーがアクセスできてしまう可能性があります。
キャッシュポイズニング:
- 悪意のある攻撃者が、正規のコンテンツを不正なコンテンツで置き換えることで、キャッシュに偽のリソースを保存させる攻撃です。これにより、他のユーザーが改ざんされたコンテンツを提供されてしまう可能性があります。これは、
Varyヘッダの不適切な設定や、キャッシュサーバーの実装の脆弱性が原因となることが多いです。Cache-Control自体は直接的な対策ではないですが、キャッシュの再検証を促すno-cacheやmust-revalidateは、攻撃されたキャッシュがすぐに更新される機会を増やします。
- 悪意のある攻撃者が、正規のコンテンツを不正なコンテンツで置き換えることで、キャッシュに偽のリソースを保存させる攻撃です。これにより、他のユーザーが改ざんされたコンテンツを提供されてしまう可能性があります。これは、
0-RTTの再送リスク:
- HTTP/3の0-RTT機能は、過去のTLSセッション情報を使ってハンドシェイクを省略し、高速なリクエストを可能にしますが、これによりリクエストの再送攻撃(replay attack)のリスクが生じます。
GETリクエストなど冪等な操作であれば問題ありませんが、POSTリクエストのような非冪等な操作を含むリクエストがキャッシュされていると、意図しない複数回実行される可能性があります。Cache-Control: no-cacheやno-storeをPOSTリクエストの結果に適用することで、このリスクを軽減できます。
- HTTP/3の0-RTT機能は、過去のTLSセッション情報を使ってハンドシェイクを省略し、高速なリクエストを可能にしますが、これによりリクエストの再送攻撃(replay attack)のリスクが生じます。
実装メモ
1. キャッシュキーの設計
キャッシュキーは、リソースを一意に識別するために重要です。URLだけでなく、Varyヘッダで指定されたリクエストヘッダ(例: Accept-Encoding, User-Agent, Authorizationなど)もキャッシュキーの一部として考慮する必要があります。これにより、ユーザーエージェントや言語設定などに応じて異なるバージョンのリソースを適切にキャッシュできます。
2. Path MTU (Maximum Transmission Unit) Discovery
HTTP/3が利用するQUICプロトコルでは、Path MTU Discovery (PMTUD) が重要です。PMTUDは、ネットワーク経路上のMTUを動的に発見し、それに応じたパケットサイズで送信することで、フラグメンテーションによる性能劣化やパケットロスを防ぎます。Cache-Control自体とは直接関係ありませんが、HTTP/3におけるネットワーク層の最適化は、キャッシュがミスした際のリソース取得性能に大きく影響します。
3. HOL (Head-of-Line) Blocking回避
HTTP/2のストリーム多重化、HTTP/3のQUICストリームは、それぞれアプリケーション層、トランスポート層でのHOL Blockingを回避します。これにより、キャッシュミスが発生した際も、他のリソースのダウンロードがブロックされず、全体的なページロード性能が向上します。Cache-Controlでキャッシュヒット率を高めることは依然重要ですが、ミスした際の影響は従来のプロトコルよりも緩和されます。
4. キュー制御と優先度
HTTP/2およびHTTP/3では、リクエスト/レスポンスストリームに優先度を設定できます。これにより、ブラウザは重要なリソース(HTML、CSS、同期スクリプト)を優先してダウンロードし、描画を高速化できます。Cache-Controlでキャッシュの鮮度と可能性を適切に設定することで、優先度の高いリソースがキャッシュヒットし、ネットワーク転送が不要になることで、結果的にキューイング遅延の削減に貢献します。
5. CDNとの連携
Content Delivery Network (CDN) は、エッジロケーションでコンテンツをキャッシュし、ユーザーに地理的に近い場所から提供することで、遅延を最小限に抑えます。CDNとCache-Controlを連携させる際は、特にs-maxageディレクティブが有効です。CDNのような共有キャッシュに対してはs-maxageで長い期間を、クライアントのプライベートキャッシュに対してはmax-ageで異なる期間を設定することで、柔軟なキャッシュ戦略を実現できます。
まとめ
RFC 9110が定義するCache-Controlヘッダは、現代のWebパフォーマンスとセキュリティにおいて極めて重要な役割を担います。no-cache、max-age、must-revalidateなどのディレクティブを適切に利用することで、開発者はリソースの鮮度、キャッシュ可能性、再検証ポリシーをきめ細かく制御し、アプリケーションの応答性向上とサーバー負荷軽減を実現できます。
HTTP/2やHTTP/3といった新しいプロトコルが提供するネットワークレベルの最適化と組み合わせることで、Cache-Controlはさらに効果を発揮します。ネットワークエンジニアとしては、単にディレクティブを記述するだけでなく、その背後にあるキャッシュのライフサイクル、セキュリティリスク、そしてプロトコルスタック全体での相互作用を深く理解し、アプリケーションの特性に応じた最適なキャッシュ戦略を設計・実装することが求められます。
参考文献
- [1] Fielding, R., Nottingham, M., & Reschke, J. (2022, June). RFC 9110: HTTP Semantics. RFC Editor. https://www.rfc-editor.org/rfc/rfc9110.html

