HTTPキャッシュ制御の完全ガイド:Cache‑Control・ETag・CDNで高速化とセキュリティを両立

キャッシュ制御とは何か — 基本概念の整理

キャッシュ制御(Cache Control)は、HTTP レスポンスやリクエストに付加される指示(ヘッダー)を通じて、ブラウザやプロキシ、CDN などの中間キャッシュに対して「いつ」「どのように」「どこまで」コンテンツを保存・再利用してよいかを決める仕組みです。適切なキャッシュ制御は応答速度の向上、帯域削減、サーバ負荷軽減に直結しますが、誤った設定は古いコンテンツの配信やセキュリティ問題(個人情報の漏洩)を招きます。

キャッシュの種類とキャッシュ層(キャッシュ階層)

  • ブラウザキャッシュ(クライアント側):ユーザーのブラウザが保持する。レスポンスの再利用が最も素早く行われる。
  • プロキシキャッシュ / 共有キャッシュ:ISP や企業のプロキシ、CDN など複数ユーザーで共有される。レスポンスが「共有可能(public)」かどうかが重要。
  • リバースプロキシ / CDN:オリジンサーバーの前段で高速化を提供する。s-maxage など共有キャッシュ専用の指示が適用される。
  • Service Worker / Cache API:アプリ側で細かく制御できるキャッシュ。オフライン対応や戦略的キャッシュに使う。

代表的なヘッダーとその意味

  • Cache-Control(最も重要): 現代の HTTP では主にこのヘッダーで制御する。複数のディレクティブ(max-age, no-cache, no-store, public, private, s-maxage, must-revalidate, proxy-revalidate, immutable, stale-while-revalidate, stale-if-error など)を組み合わせる。
  • Expires:古い仕様だが max-age が無い場合の代替。絶対時刻で「この時刻までは有効」と指示する。
  • ETag:エンティティタグ。レスポンスに固有の識別子を付け、If-None-Match による条件付きリクエストで差分チェック(検証)に使う。
  • Last-Modified:最終更新日時。If-Modified-Since による条件付きリクエストに使われるが、時刻単位の精度や動的コンテンツでの正確性に限界がある。
  • Vary:どのリクエストヘッダーの値によってレスポンスが異なるかを示す。例:Vary: Accept-Encoding、Vary: Origin。

主な Cache-Control のディレクティブと使い方

  • max-age=秒数:レスポンスの鮮度有効時間。ブラウザや中間キャッシュはこの秒数以内は再検証なしで利用できる。
  • s-maxage=秒数:共有キャッシュ(CDN など)専用の max-age。存在する場合、共有キャッシュはこれを優先する。
  • public / private:public は共有キャッシュに保存可能、private はユーザー単位のキャッシュのみ許可(プロキシは保存しない)を示す。
  • no-cache:キャッシュに保存はできても、再利用時に必ずオリジンへ検証(If-None-Match / If-Modified-Since)を行う必要がある。
  • no-store:絶対に保存しない。銀行や医療など機密データのレスポンスに必須。
  • must-revalidate / proxy-revalidate:鮮度切れの際には必ずオリジンに再検証することを強制。proxy-revalidate は共有キャッシュに対してのみ有効。
  • immutable:リソースが当分変わらない場合に使い、ユーザーの再アクセス時でも再検証を抑制する(主に長期版の静的アセットに有効)。
  • stale-while-revalidate / stale-if-error:古いコンテンツを許容して非同期再検証(あるいはエラー時の利用)を許す拡張指示(RFC 5861)。

検証(Validation)と期限(Expiration)の違い

Expiration ベース(max-age/Expires)は「期限が切れるまで再検証不要」とする方式、検証ベース(ETag/Last-Modified + If-None-Match/If-Modified-Since)は「サーバーに変化があるか問い合わせて差分だけ返す」方式です。多くの実装では両者を組み合わせ、まず期限内は再検証不要、期限切れ後に条件付きリクエストで 304 Not Modified を受けて差分のみ確認、という流れになります。

実務でよく使うパターン(推奨例)

  • 静的アセット(CSS/JS/画像):
    • Cache-Control: public, max-age=31536000, immutable
    • ビルド時にファイル名にハッシュを付与(content-hash)して長期キャッシュを有効活用する。
  • HTML(動的ページ):
    • 短めの max-age(例 0 または数秒)+ no-cache または must-revalidate を付け、頻繁に更新がある場合はサーバー側で差分検証を行う。
  • API レスポンス:
    • 認証が必要なレスポンスは Cache-Control: private, no-store または短い max-age を検討。公的にキャッシュしてよい場合は s-maxage を設ける。

キャッシュバスティング(破壊)戦略

キャッシュバスティングは「更新したアセットを確実にクライアントが取得できるようにする」技法です。代表的な手法:

  • ファイル名バージョニング(例 app.9f3a2c.js) — 最も安全で広く使われる。
  • クエリ文字列(?v=1.2.3) — 一部 CDN やプロキシでクエリ無視設定があるため注意。
  • ヘッダーで短いキャッシュにして、頻繁に更新されるものは強制的に再取得させる。

よくある落とし穴・注意点

  • Vary ヘッダーを忘れると、Accept-Encoding(gzip/brotli)や Origin(CORS)で不正なレスポンスが配信される可能性がある。Vary: Accept-Encoding は圧縮版と非圧縮版を区別するため必須。
  • ユーザー固有の情報を共有キャッシュに保存しない(必ず private または no-store)。
  • ETag を環境間で生成方法が違うと、同一リソースでも不必要に 200 を返してしまう(ETag はコンテンツに基づく一貫性のある生成が望ましい)。
  • 長い max-age とハードコーディングされた URL を組み合わせずに使うと、更新がユーザーに届かない問題が発生する。
  • Vary: * は一般に「キャッシュ不可」を意味し、ほとんどの共有キャッシュは Vary: * のレスポンスを保存しない。

実際の設定例(Nginx / Apache / CloudFront)

(一例)Nginx で静的アセットを長期キャッシュにする設定:

location ~* \.(?:css|js|jpg|jpeg|png|svg|woff2?)$ {
  add_header Cache-Control "public, max-age=31536000, immutable";
}

Apache(mod_headers)で HTML を再検証させる例:

<FilesMatch "\.html$">
  Header set Cache-Control "no-cache, must-revalidate"
</FilesMatch>

CloudFront ではオリジンからの Cache-Control / Expires を尊重するか、オリジンの代わりにキャッシュポリシーで上書きする設定が可能です。CDN の挙動はプロバイダ毎に違うため、ドキュメントを確認してテストすること。

デバッグと確認方法

  • curl でヘッダー確認:curl -I https://example.com/path
  • 条件付きリクエストの挙動確認:curl -I -H "If-None-Match: "etag-value"" ...
  • ブラウザの DevTools(Network タブ)でレスポンスヘッダーとキャッシュヒット/ミスを確認。
  • CDN のキャッシュヒット率やログを監視して、設定の効果を測定する。

セキュリティとプライバシー観点

個人情報や認証情報を含むレスポンスは決して共有キャッシュに放出してはならない(必ず Cache-Control: private または no-store を利用)。また、HTTPS を使っているからといってキャッシュ設定を怠ると、クライアント端末上に残存してしまうリスクがあるため注意が必要です。CORS とキャッシュの組み合わせでは、Vary: Origin を付けることで別オリジンごとに分けてキャッシュできます。

最新の拡張とブラウザ対応

stale-while-revalidate / stale-if-error や immutable は比較的新しい指示で、多くのブラウザや CDN がサポートしていますが、挙動は完全に統一されているわけではありません。実運用ではターゲットユーザーのブラウザシェアや CDN のサポートを確認してから採用を判断してください。

まとめと実務的なおすすめフロー

  • 静的アセットはファイル名バージョニング+長期キャッシュ(immutable)を推奨。
  • HTML や頻繁に変わる API は短い有効期限+条件付きリクエスト(ETag)で差分検証を行う。
  • 認証や個人情報を含むレスポンスは private / no-store を徹底。
  • Vary ヘッダーで配信差分(Accept-Encoding / Origin 等)を正しく指定する。
  • 変更時に確実にユーザーに配信させるために、ビルドプロセスでのハッシュ付与(キャッシュバスティング)を導入する。
  • CDN 設定やプロキシの挙動はプロバイダ毎に違うため、本番での検証を入念に行う。

参考文献