HTTPキャッシュコントロールヘッダー no-cache、no-store、max-age が実際に意味すること
キャッシュ制御ディレクティブの実際の解説 — ブラウザとCDNが no-cache、no-store、max-age、s-maxage、ETag に対して実際にどう処理するか。また、開発者が生産環境で遭遇するよくある誤りも紹介します。
おそらく書いたでしょう Cache-Control: no-cache そしてブラウザがキャッシュを完全にスキップすると仮定しました。それはそうではありません。 no-cache 「キャッシュから提供する前に再検証する」と意味します。実際に応答が保存されないことを望む場合、それは no-storeです。
各ディレクティブを明確に見てみましょう — ブラウザがどのように扱うか、CDNがどのように扱うか、そしてそれらを混同した誤りです。
完全なキャッシュコントロールディレクティブ参照
| ディレクティブ | ブラウザの動作 | CDN / プロキシの動作 | 典型的な用途 |
|---|---|---|---|
max-age=N | N秒キャッシュ | N秒キャッシュ(ただし s-maxage が上書きされる場合を除く) | 静的資産、API応答 |
s-maxage=N | 無視 | N秒キャッシュ | CDNのTTLがブラウザと分離 |
no-cache | キャッシュするが、各リクエストで再検証 | キャッシュするが、各リクエストで再検証 | ETagを使用する頻繁に変化するコンテンツ |
no-store | どこにも保存しない | どこにも保存しない | 認証応答、敏感なユーザーデータ |
must-revalidate | 古いデータを提供しない — 再検証または失敗 | 古いデータを提供しない — 再検証または失敗 | 古いデータが破壊になるAPI応答 |
proxy-revalidate | 無視 | 共有キャッシュでの古いデータ提供を禁止 | CDN固有の再検証必須 |
private | ブラウザがキャッシュできる | キャッシュされない | ユーザー固有のページ |
public | どのキャッシュも保存できる | キャッシュできる | 共有静的リソース |
immutable | max-age内での再検証を一切行わない | CDNによって異なる | ハッシュ化またはバージョン化された資産 |
stale-while-revalidate=N | 新しいデータを取得しながら、N秒間古いデータを提供する | CDNによって異なる | 硬い古いデータを避けた高速性 |
max-ageとs-maxage:ブラウザとCDNのTTL
max-age=N は、応答が新鮮である秒数をブラウザとCDNに伝えます。N秒経過後、キャッシュされた応答は古くなり、使用前に再検証が必要になります。
s-maxage=N はCDN専用です。ブラウザはまったく無視します。CDNが1時間キャッシュし、ブラウザが5分だけキャッシュしたい場合:
Cache-Control: max-age=300, s-maxage=3600
ブラウザは5分間キャッシュします。CloudFront、Fastly、Nginxなどは3600秒を使用します。よくある誤り: max-age=0 を設定してキャッシュを無効にしていると思い込んでいます。それはそうではありません。 max-age=0 は応答が即座に古くなることを意味します — ブラウザはまだキャッシュし、再検証を行い、おそらく304を取得します。キャッシュを一切行わない場合は、 no-store.
no-cache: あなたが思っていることではない
Cache-Control: no-cache 「キャッシュを使わない」と意味しません。それは「サーバーから再検証せずにキャッシュから提供しない」と意味します。
ブラウザがキャッシュされた応答を持つ場合のシーケンス no-cache:
- 新しいリクエストがキャッシュされたリソースに対して到達
- ブラウザがキャッシュされた応答のETag(
If-None-Match)またはLast-Modifiedタイムスタンプをサーバーに送信 - コンテンツが変更されていない場合 → サーバーは
304 Not Modifiedを返す(ボディなし) - コンテンツが変更された場合 → サーバーは
200 OKを返す(新しい応答)
The benefit over no-storeより:304応答による帯域幅の節約を維持できます。コンテンツがまれに変更され、変更時には新鮮である必要がある場合、 no-cache とETagを組み合わせるのが正しい組み合わせです。
no-store: 実際の「キャッシュしない」
Cache-Control: no-store は応答がどこにも保存されないことを意味します — ブラウザキャッシュ、CDN、中間プロキシにすべて保存されません。コピーは一切ありません。
次の用途で使用します:
- 認証応答(ログイントークン、セッションデータ)
- 敏感な個人データ
- 一時的なコンテンツ(支払い確認、OTPページ)
一つの細部: no-store は、ページがブラウザのバックフォワードキャッシュ(bfcache)に表示されることを防げません。ブラウザはナビゲーションパフォーマンスのために、HTTPキャッシュとは別にメモリ内のスナップショットを保持します。ログアウト後のバックボタン問題を処理する必要がある場合は、 pageshow イベントに接続し、 event.persisted.
must-revalidate: 古いデータの許容なし
HTTPキャッシュ仕様は、オリジンがアクセスできない場合に、キャッシュが古い応答を提供できるようにする機能を許容しています — ほとんどの開発者が知らないリズムです。 must-revalidate はその余地を削除します:キャッシュされた応答が古くなった後、キャッシュは再検証または504を返す必要があります。どんな場合でも古いデータを提供しません。
# Without must-revalidate: CDN may serve stale if origin is slow or down
Cache-Control: max-age=3600
# With must-revalidate: stale = error, not a fallback
Cache-Control: max-age=3600, must-revalidate
機能が破壊されるAPI応答(在庫数、価格、認証状態)に使用します — ただ見た目が少し間違っているだけではなく。
private vs public: CDNがユーザーデータを漏らすバグ
private は応答が特定のユーザー向けであることを意味します。ブラウザはそれをキャッシュできますが、共有キャッシュ(CDN、リバースプロキシ)はキャッシュしてはなりません。
public は、すべてのキャッシュ — 包括して共有キャッシュ — が応答を保存できるように明示的に許可します。一部のキャッシュは、明示的にマークしなければ認証リクエストの応答のみをキャッシュします。 public.
この現実のバグ:開発者が静的資産から Cache-Control: public, max-age=3600 をページにコピーします。CDNが応答をキャッシュします。ユーザーBが同じリクエストを実行し、ユーザーAのページをキャッシュから取得します。これは理論的ではありません — GitHubは2018年にこのバージョンを経験しました。認証またはユーザー固有の応答を private 明示的にマークしてください、あなたがCDNがそれをキャッシュしないと感じているとしても。
ETagと条件付きリクエスト
ETagはサーバーが「この応答の指紋」を示す方法です。ブラウザはETagをキャッシュされた応答とともに保存し、次のリクエスト時に If-None-Matchを介してサーバーに送信します。コンテンツが変更されていない場合、サーバーは 304 Not Modified を返し、ボディなし — これは no-cacheと同じ鮮度制御であり、帯域幅を大幅に節約します。
の no-cache + ETagフロー:
→ GET /api/config HTTP/1.1
← HTTP/1.1 200 OK
Cache-Control: no-cache
ETag: "abc123"
[full response body]
→ GET /api/config HTTP/1.1
If-None-Match: "abc123"
← HTTP/1.1 304 Not Modified
[no body — browser uses its cached copy]
2つのETagタイプ:
- 強力なETag (
"abc123") — バイトごとに完全に一致。CDNの範囲リクエストサポートに必須です。 - 弱いETag (
W/"abc123") — セマンティック的に同等だが、必ずしもバイト一致ではない。ブラウザの再検証には十分ですが、範囲リクエストには不向きです。
NginxはファイルのmtimeとサイズからETagを自動生成します。ExpressはデフォルトでETagを追加しません — 明示的に app.set('etag', 'strong') または etag ミドルウェアを使用してください。
Last-ModifiedとIf-Modified-Since
ETagと同様の概念ですが、粗いもの — タイムスタンプベースではなくコンテンツハッシュベースです。サーバーは Last-Modifiedを含みます;ブラウザは次のリクエストで If-Modified-Since を送信します。
問題:リデプロイ時にファイルの変更時間が更新されてもコンテンツが変わらなかった場合、キャッシュが無駄に無効になります。コンテンツハッシュベースのETagはこの問題を解決しません。ETagを使用できる限り、Last-ModifiedはETagをサポートしないサーバーのためのフォールバックとして扱ってください。
Vary: キャッシュを無意識に増加させるヘッダー
の Vary ヘッダーは応答が他のリクエストヘッダーに基づいて異なることをキャッシュに伝えます。そのヘッダーの各ユニークな組み合わせが個別のキャッシュエントリになります。
Vary: Accept-Encoding
これはキャッシュがgzip、brotli、identityエンコーディングに対して別々の応答を保存できるようにします。正しいかつ一般的です。危険なのは: Vary: Cookieです。すべてのユーザーが独自のクッキーを設定しているため、すべてのユーザーが独自のキャッシュエントリを得ます — つまり共有キャッシュを効果的に無効にします。多くのフレームワークは Vary: Cookie を無意識に追加します。CDNキャッシュヒット率が、 max-age の値が大きくても不思議に低い場合、応答ヘッダーに Vary: Cookie がセッションミドルウェアから無意識に含まれているか確認してください。
Vary: * は実際には「キャッシュしない」と意味します — すべてのリクエストがユニークと扱われます。CDNにおいてこれは no-store に等しいです。
クエリパラメータによるキャッシュの破壊
バージョン化された資産の新しいダウンロードを強制する必要がある場合、クエリパラメータを追加するのは標準的なアプローチです — クエリ文字列はURLの一部であり、ブラウザとCDNによって新しいリソースと扱われます:
/app.js?v=2.1.4
/styles.css?hash=a1b2c3d4e5f6
コンテンツハッシュやバージョン文字列から動的にキャッシュ破壊パラメータを構成する場合、特別な文字を含む場合、それらをパーセントエンコードして追加してください。テストまたはURLを手動で構築している場合、 URLエンコーダー/デコーダー がそれを迅速に処理します。
開発者が頻繁に犯す3つの誤り
1. no-cacheを使用してno-storeを意味している。 認証応答、ログアウトエンドポイント、PIIを含む場合、あなたが望むのは no-store. no-cache で、データがブラウザキャッシュに残ります(ただ古くなっているとマークされています); no-store はその足跡を完全に削除します。ユーザーが共有デバイスを使用する場合、その違いが重要になります。
2. CDN制御のためにs-maxageを設定していない。 .yml としてダウンロード s-maxage、あなたのCDNは max-ageを使用します。もし max-age が短い(例:60秒)なら、あなたのCDNも60秒間キャッシュします — おそらくそれは望ましくない結果です。それぞれのTTLを明確に分離してください。
3. ユーザーデータを返すエンドポイントにpublicを設定している。 これはセキュリティ事故であり、単なるパフォーマンスバグではありません。パーソナライズされたまたは認証された応答は privateでなければなりません。デフォルトは private で、 public を、本当に共有されるリソースにのみ設定してください。
まとめ
メンタルモデル: no-cache は鮮度制御に関するもの — 応答はキャッシュに存在し、使用前にサーバーからの承認が必要です。 no-store は痕跡を残さないということです。 max-age はあなたのブラウザのTTLです。 s-maxage はあなたのCDNの別個のTTLです。ETagは再検証を安価にします。
ユーザーデータに触れるすべてのエンドポイントで、 private/public の区別を正しく理解してください。その1つの誤り — 静的資産からキャッシュヘッダーを認証エンドポイントにコピーする — あなたのCDNがパーソナライズされた応答をキャッシュ開始すると、ユーザー間のデータ漏洩に発展します。
恵 スコアボードが到着しました!
スコアボード ゲームを追跡する楽しい方法です。すべてのデータはブラウザに保存されます。さらに多くの機能がまもなく登場します!
