max-ageは何秒にすべきか
2026年9月26日
著者: 竹洞 陽一郎
主なポイント
- max-ageは「問い合わせなしで使ってよい期間」の宣言であって保存期間ではないため、長いmax-ageは、ブラウザが1年よりはるかに早く追い出すので性能上の期待値が低く、同じURLで内容を差し替えるリソースでは古い内容が使われ続けるので危険です。
- 最適なmax-ageは「内容が変わったときにURLも変わるか」で決まり、ハッシュやバージョン付きURLの静的ファイルは max-age=31536000, immutable、HTMLは no-cache+ETag か60〜300秒程度の短いmax-age、URLが変わらないファイルは更新を反映させたい時間で決めます。
- s-maxageを使うと、「Cache-Control: max-age=60, s-maxage=86400」のようにブラウザとCDNに別々の有効期限を指定でき、CDNには長く保持させて更新時にPurgeしますが、有効にするための前提設定はCDNごとに異なります。
- ChromiumとFirefoxのソースコードでは、追い出しの判断にmax-ageは使われていないため、max-ageを短くしても容量は空かず、ストレージの制約に対して本当に効く設計変数は、サイトの総リソース量とCDNへの依存度です。
- バージョン付きのファイル名のまま中身を上書きすると、修正前の内容が最長1年間ブラウザに残り、CDNのPurgeでは直せないため、ファイル名の変更をビルドの仕組みか運用手順で強制します。
「1年」は正しいが、理由を知っている人は少ない
静的ファイルのCache-Controlに max-age=31536000(1年)を指定するのは、広く知られた慣習です。
一方で、HTMLや画像のように更新されるファイルに何秒を指定すべきかは、サイトごとに判断が分かれます。
「モバイルはキャッシュ容量が限られているから短くすべきだ」という意見も聞きます。
この記事では、max-ageの決め方を、更新の反映と、ブラウザのキャッシュの追い出しの仕組みの2つから説明します。
前提として、ブラウザキャッシュは何日残るのかで説明したとおり、ブラウザは指定した期間よりはるかに早くキャッシュを追い出します。
その事実を踏まえたうえで、それでも何秒を指定すべきかを考えます。
なぜキャッシュの寿命を確率で扱うのか、という理論的な背景はキャッシュの寿命はなぜ確率で扱うのかで、自分のサイトで実測する方法はブラウザキャッシュのヒット率を生存時間解析で実測するで説明しています。
先に押さえること:max-ageは「新鮮さ」の宣言であって、保存期間ではない
max-ageの意味を取り違えると、この先の推奨値の理由が分からなくなるので、先に押さえておきます。
RFC 9111のSection 5.2.2.1では、max-ageレスポンスディレクティブは、レスポンスの経過時間(age)が指定秒数を超えたらstale(古い)とみなす、と定義されています。
そしてSection 4.2では、fresh(新鮮)なレスポンスとは、キャッシュがオリジンサーバーに問い合わせずに再利用してよいレスポンスのことだとされています。
つまり、max-age=31536000 は、「このURLのこのレスポンスは、1年間は問い合わせなしで使ってよい」というオリジンサーバーの宣言です。
「1年間保存せよ」でも「内容は最新である」でもありません。
この1つの定義から、正反対に見える2つの結論が同時に導かれます。
- 長いmax-ageは、性能上の期待値が低い。保存を保証しないので、ブラウザは1年よりはるかに早く追い出します(ブラウザキャッシュは何日残るのか)
- 長いmax-ageは、内容が変わり得るURLには危険である。問い合わせをしないことを許すので、同じURLで内容を差し替えても、ブラウザは追い出されるまで古い内容を使い続けます。オリジンでファイルを差し替えても、CDNのキャッシュを削除しても、ブラウザには届きません
「1年間問い合わせなしで使ってよい」という宣言が真実なのは、そのURLの内容が将来にわたって変わらない場合だけです。
それを成り立たせる方法が、ファイル名にバージョンやハッシュ値を入れる運用です。
Google Chromeチームが運営するweb.devの「Prevent unnecessary network requests with the HTTP Cache」では、内容が変わるたびにURLを変える方法として、ファイルのフィンガープリント(内容から計算したハッシュ値)やバージョン番号をファイル名に埋め込むこと(例:style.x234dff.css)が示されています。
Google検索セントラルの「Fix Search-related JavaScript problems」も、Googlebotが古いCSSやJavaScriptを使ってしまう問題への対策として、同じ方法(例:main.2bb85551.js)を挙げています。
base.20260925.css のように日付を入れる書き方も、同じ考え方です。
内容が変わるたびにURLが変わる運用であれば、このURLの内容は変わらないので、宣言は真実です。
更新があれば新しいURLが使われ、古いURLは参照されなくなります。
一方、/img/logo.png や HTML のように、同じURLで内容を差し替えるリソースに1年を付けると、宣言は虚偽になります。
最適なmax-ageが「内容が変わったときにURLも変わるか」で決まるのは、このためです。
なお、この運用には落とし穴があります。
バージョン付きのファイル名のまま中身だけを上書きすると、修正前の内容が最長1年間ブラウザに残り、CDNのPurgeでは直せません。
また、新しいURLへの参照はHTMLに書かれているので、HTML自体が短い有効期限(no-cacheや数分)であることが前提です。
HTMLが古いままなら、新しいCSSは読み込まれません。
よくある事故は、次の3つです。
- HTMLのブラウザ向け有効期限は短いのに、CDN向けのs-maxageが長く、しかも更新時のPurgeの対象からHTMLが外れている
- CSSとJavaScriptはバージョン付きURLにしたのに、画像だけ /img/hero.jpg のようなURLのまま1年の有効期限が付いている
- デプロイの手順で、バージョン付きのファイル名のまま中身を上書きしてしまう
では、max-ageは何秒にすべきか
最適な値は1つではなく、「内容が変わったときにURLも変わるか」で決まります。
URLが変わるリソースは長く、URLが変わらないリソースは短く(または毎回再検証)が基本です。
再検証とは、キャッシュを使う前にサーバーへ更新の有無を問い合わせることで、更新がなければ304 Not Modifiedが返り、本文は転送されません。
| リソース | ブラウザ向け | CDN向け |
|---|---|---|
| ハッシュやバージョン付きURLの静的ファイル(CSS、JS、フォント、画像) | max-age=31536000, immutable(※1) | 同じ、またはさらに長く |
| HTML | no-cache+ETag(※4)、または短いmax-age(60〜300秒程度) | s-maxage(※3)を長めに設定し、更新時にPurge(※2) |
| ハッシュなしの静的ファイル | 1時間〜1日程度+ETag | 長めに設定し、更新時にPurge(※2) |
| 同じURLのまま差し替えたい画像(ロゴ、ファビコン、OGP画像など) | 差し替えを反映させたい時間(例:1時間)+ETag | 長めに設定し、差し替え時にPurge(※2) |
| API・個人向けデータ | private、またはno-store | キャッシュしない |
- ※1 immutable
-
RFC 8246で定義されたCache-Controlの拡張ディレクティブです。
「有効期限内に更新されることはない」という宣言で、これが付いたレスポンスは、ユーザーが再読み込みしても有効期限内は再検証されません。
対応状況はブラウザによって異なり、未対応のブラウザでは単に無視されます(付けることによる害はありませんが、付ければ必ず再読み込みが抑止されるわけでもありません)。
ディレクティブの詳細と各ブラウザの対応状況は、ブラウザキャッシュは何日残るのかで説明しています。 - ※2 Purge
-
CDNに保持されているキャッシュを管理画面やAPIから削除する操作です。
キャッシュクリアとも言います。 - ※3 s-maxage
-
RFC 9111のSection 5.2.2.10で定義されたCache-Controlのレスポンスディレクティブです。
RFC 9111は、キャッシュを「共有キャッシュ」(CDNやプロキシのように、複数のユーザーで共有されるキャッシュ)と「プライベートキャッシュ」(ブラウザのように、1人のユーザー専用のキャッシュ)に分けています。
s-maxageは共有キャッシュにだけ適用され、共有キャッシュではmax-ageやExpiresより優先されます。
つまり、「Cache-Control: max-age=60, s-maxage=86400」と書くと、ブラウザは60秒、CDNは1日という、別々の有効期限を1つのヘッダーで指定できます。
CDNには長く保持させて更新時にPurgeし、ブラウザには短い期限を指定して更新を早く反映させる、という使い分けのための仕組みです。
また、s-maxageにはproxy-revalidateの意味が含まれており、共有キャッシュは期限切れ(stale)のレスポンスを、オリジンで検証するまで再利用してはならない(MUST NOT)とされています。
主要なCDNがs-maxageをどう扱うかは、この節の末尾の補論にまとめています。
導入時には、そこにある「有効にするための前提設定」の欄を確認してください。 - ※4 HTMLの no-cache と max-age=0, must-revalidate
-
表のHTMLの行で挙げたno-cacheは、「使う前に必ずサーバーへ確認する」という意味で、ETagかLast-Modifiedがあれば304で済みます。
max-age=0, must-revalidate もほぼ同じ挙動になりますが、no-cacheのほうが意図が明確で、多くのCDNの扱いも一貫しています。
どちらの場合も、HTMLがキャッシュされないわけではなく、「確認なしでは使われない」だけです。
ハッシュ付きURLのファイルに1年を指定するのは妥当ですが、1年より長くしても効果はほとんどありません。
ブラウザキャッシュは何日残るのかで説明したとおり、ブラウザは1年よりはるかに早く追い出すからです。
RFC 9111のSection 5.3にも、HTTPはかつてExpiresの値を1年先までに制限していたと書かれており、1年はその名残りの慣習的な上限です。
最適値を決めるのに必要な情報は、「更新頻度」ではなく「更新後、何秒以内に全ユーザーへ反映させる必要があるか」です。
その許容時間がブラウザ向けのmax-ageの上限になります。
CDN向けは、Purgeで即時に無効化できるため、ブラウザ向けより長く設定できます。
s-maxageは共有キャッシュ(CDN)にだけ適用され、max-ageより優先されます。
(RFC 9111 Section 5.2.2.10)
補論:主要CDNでのs-maxageの扱い
表1の「CDN向け」の列を使うには、利用しているCDNがs-maxageをどう扱うかを知っておく必要があります。
主質問(何秒にすべきか)への答えは表1で終わっているので、この補論は、CDNを導入している場合、または導入を検討している場合に読んでください。
表は横にスクロールできます。
| CDN | s-maxageの扱い | 有効にするための前提設定 | max-ageと両方ある場合 | 期限切れ後の挙動 |
|---|---|---|---|---|
| Akamai |
尊重する。 s-maxageの値でエッジにキャッシュし、max-ageとs-maxageの両方をそのまま下流(ブラウザ)へ送る。 |
既定ではオリジンのCache-Controlを尊重しない。 Property Managerの「Caching」で「Honor origin Cache-Control and Expires」を選択し、s-maxageの尊重にはEnhanced RFC supportを有効にする。 |
s-maxageを優先 |
「Force revalidation of stale objects」の設定で、検証できない場合にstaleを返すかどうかを選べる。 proxy-revalidateを尊重する設定がある。 |
| Cloudflare |
尊重する。 s-maxageでエッジTTL、max-ageでブラウザTTLを別々に指定できる。 |
Origin Cache ControlはFree/Pro/Businessで既定で有効。 Enterpriseは有効化が必要。Cache RulesでEdge TTLを固定値にすると、そちらが優先される。 HTMLは既定ではキャッシュ対象外のため、別途Cache Rulesが必要。 |
s-maxageを優先 | RFC 9111に従い、s-maxageはproxy-revalidateの意味を含むため、オリジンで再検証せずにstaleを返さない。 s-maxageがあるとstale-while-revalidateは無効になる。 |
| Fastly |
尊重する。 優先順位は Surrogate-Control: max-age、Cache-Control: s-maxage、Cache-Control: max-age、Expiresの順。 Surrogate-Controlは下流へ送る前に削除されるが、s-maxageは削除されない。 |
標準のVCLボイラープレートで既定から尊重する。 Cache-Control: privateまたはSet-Cookieがあればキャッシュしない。 |
s-maxageを優先 |
TTL切れ後は、再検証できなければ使わない。 staleを返すのは、stale-while-revalidateまたはstale-if-errorを指定した場合。 |
| Amazon CloudFront |
尊重する。 max-ageとs-maxageの両方があれば、CloudFrontはs-maxageで、ブラウザはmax-ageでキャッシュする。 |
Cache PolicyのMinimum TTL / Maximum TTL / Default TTLと組み合わされ、s-maxageの値はMinimum TTLとMaximum TTLの範囲に丸められる。 Minimum TTLが0より大きいと、no-cache / no-store / privateが無視される。 |
s-maxageを優先 |
期限切れ後は、次のリクエストでオリジンに問い合わせて検証する。 staleを返すのは、stale-while-revalidateまたはstale-if-errorを指定した場合(値はMaximum TTLで頭打ち)。 |
| Gcore |
尊重する。 公式ドキュメントに記載はないが、弊社の問い合わせに対し、2026年9月25日に「max-age=60, s-maxage=600 の場合、CDNは600秒キャッシュする」との回答を得た。 |
「Origin controlled」が既定。 オリジンがキャッシュ関連ヘッダーを返さない場合は「Default cache expiry」(既定4日)を使う。 オリジンが返すAgeヘッダーがs-maxageより大きいと、staleとみなされキャッシュされない。 |
s-maxageを優先 |
期限切れ後は、ETagまたはLast-Modifiedがあれば条件付きリクエストで再検証する。 Always Onlineが無効(既定)のときはstaleを一切返さない(エッジのNGINXで proxy_cache_use_stale が off になる)。 staleを返すのは、Always Onlineを有効にした場合(オリジンのエラー・到達不能・更新中)。 stale-while-revalidate / stale-if-error は評価されない。 CDN-Cache-Controlは非対応。 36時間リクエストがないファイルは、鮮度に関係なく削除される。 |
ストレージの制約とmax-ageの関係
この節で扱うのは、ブラウザのHTTPディスクキャッシュです。
タブが開いている間だけ有効なメモリキャッシュ、「戻る/進む」で使われるBack/Forward Cache、Service WorkerのCache APIは、それぞれ別の仕組みで、ここでの説明の対象外です。
また、ソースコードで確認したのはChromiumとFirefoxで、Safari(WebKit)のディスクキャッシュについては同等の確認をしていません。
「max-ageは追い出しに使われない」という結論は、この2つの実装について確認したものです。
max-ageは追い出しの判断に使われない
「モバイルはキャッシュの容量が限られているから、max-ageを短くして容量を節約すべきではないか」と考える方がいますが、これは成り立ちません。
ChromiumとFirefoxの実装では、追い出しの基準は最終利用時刻(Chromium)やfrecency(Firefox)であり、レスポンスのmax-ageは追い出しの判断に使われません。
max-ageが1日でも1年でも、エントリが占有する容量は同じで、追い出される順番も同じです。
max-ageが決めるのは、「エントリが残っていたときに、再検証なしで使ってよいか」だけです。
これは仕様上の規定ではなく、実装の挙動なので、ソースコードで示します。
Chromiumの追い出し処理(net/disk_cache/simple/simple_index.cc、SimpleIndex::StartEvictionIfNeeded、2026年9月25日時点のmainブランチ)では、各エントリの並べ替えの値を次のように計算しています。
const uint64_t time_since_last_used = now - i->second.RawTimeForSorting();
uint64_t sort_value = time_since_last_used;
// See crbug.com/736437 for context.
if (use_size_heuristic) {
sort_value *= i->second.GetEntrySize() + kEstimatedEntryOverhead;
...
}
並べ替えの値は「最終利用からの経過時間 × エントリのサイズ」で、この値が大きいエントリから追い出されます。
なお、この抜粋の直後には、高優先度のヒント(HINT_HIGH_PRIORITY)が付いたエントリのうち最終利用から1日以内のものについて、並べ替えの値を10で割る処理があります(kSimpleCachePrioritizedCaching、2026年9月25日時点のmainブランチで既定で有効)。
高優先度のエントリは追い出されにくい、ということです。
どのリソースにこのヒントが付くかは、この記事では確認していません。
いずれにしても、レスポンスのmax-ageや有効期限は、この計算に一切現れません。
また、Firefoxの追い出し処理(netwerk/cache2/CacheIndex.cpp、CacheIndex::GetEntryForEviction)には、次のコメントとともに、frecencyで並べ替えた配列を先頭から走査するコードがあります。
// find first non-forced valid and unpinned entry with the lowest frecency index->mFrecencyArray.SortIfNeeded(lock);
こちらも、frecency(利用頻度と最終利用時刻)だけが基準で、max-ageは使われていません。
なお、Chromiumの計算式にエントリのサイズが掛かっていることは、後述する「サイトの総リソース量」がキャッシュの生存に直接効く根拠でもあります。
同じ時間放置されたエントリなら、大きいファイルのほうが先に追い出されます。
では、ストレージの制約は、max-ageの決め方にどう関係するのでしょうか。
ここで問題になるのは、「あるファイルがブラウザのキャッシュに、いつまで残っているか」が事前には分からないことです。
追い出しは、そのユーザーが他のサイトをどれだけ見たかで決まるので、同じファイルでも、あるユーザーの端末では3日残り、別のユーザーの端末では6時間で消えます。
つまり、「t 時間後に残っているかどうか」は、ユーザーごとに違う、確率的な事柄です。
生存関数から見た、max-ageの実効的な上限
「前回そのファイルを読み込んでから t 経過した時点で、キャッシュがまだ残っているユーザーの割合」を、生存関数 S(t) と呼びます。
統計学の生存時間解析の道具で、なぜキャッシュにこの考え方が使えるのか、追い出しの方式によって S(t) の形がどう変わるのか、容量から S(t) を概算するとどうなるのかは、別の記事「キャッシュの寿命はなぜ確率で扱うのか」で説明しています。
ここでは、その結果だけを使います。
仮の数値で示すと、S(t) は次のような形です。
| 経過時間 t | S(t) | 意味 |
|---|---|---|
| 0 | 1.00 | 読み込んだ直後は、全員のキャッシュに残っている |
| 6時間 | 0.88 | 9割近くのユーザーの端末に残っている |
| 1日 | 0.61 | 6割 |
| 3日 | 0.22 | 2割 |
| 7日 | 0.03 | ほぼ消えている |
| 30日 | 0.00 | 誰の端末にも残っていない |
グラフにすると、次のようになります。
表の値は、この曲線上の点です。
この図の「2日で半減」という形は、説明のために置いた仮の値です。
自分のサイトの S(t) がどのような形になるかは、ブラウザキャッシュのヒット率を生存時間解析で実測するの方法で計測して初めて分かります。
この S(t) を使うと、max-ageを延ばす効果を評価できます。
max-ageを m から少し延ばして得られる利益は、「m の時点でキャッシュが残っていたのに、期限切れで再検証が必要だった」ケースが、「再検証なしでそのまま使える」ケースに変わる分です。
その利益が発生するのは、m の時点でキャッシュが残っているユーザーだけなので、利益の大きさは S(m) に比例します。
厳密には、その中でさらに「m から延ばした先の時刻までの間に再訪する」ユーザーだけが利益を受けます。
再訪がなければ、キャッシュが残っていてもmax-ageを延ばす利益はゼロです。
上の例で、max-ageを3日から7日に延ばした場合、利益を受けるのは3日〜7日の間にまだキャッシュを持っている、全体の約2割のユーザーです。
7日から30日に延ばしても、7日の時点で残っているのは3%なので、利益はほとんどありません。
つまり、S(t) が十分小さくなる時刻が、「これ以上長くしても意味がないmax-age」の実効的な上限です。
URLにバージョンを入れたリソースは、古い内容が表示されるコストがないので、この上限以上であれば何を指定しても結果は同じです。
1年で十分だというのは、この意味です。
ストレージの制約に対して本当に効く設計変数
S(t) が速く0に近づくのは、キャッシュの容量が小さい端末や、他のサイトを多く見るユーザーです。
純粋なLRU(最も長く使われていないものから追い出す方式)を仮定して容量から概算すると、典型的なユーザーで2〜7日程度、閲覧の多いユーザーでは1日以下で消えます(計算の根拠はキャッシュの寿命はなぜ確率で扱うのかの「キャッシュ容量から生存関数を概算する」を参照)。
この概算で仮定した容量320MiBは、Chromiumのソースコード(net/disk_cache/cache_util.cc)で、Windows以外のディスクキャッシュの基準値80MiBを4倍した目安です。
実際の上限は端末の空き容量に応じて決まり、この目安の4倍まで拡大されることもあれば、空き容量の少ないモバイル端末ではこれより小さいこともあります。
「2〜7日」は、この目安と仮定した閲覧量から出した値であり、実測値ではありません。
「1年」というmax-ageの値は、この日数からかけ離れています。
ストレージの制約が本当に効く設計変数は、max-ageではなく、次の2つです。
- サイトの総リソース量:エントリ数とバイト数が多いほど、自サイトのリソースが他サイトの取得で押し出されやすくなります。ファイルの統合と削減は、キャッシュの生存にも効きます
- CDNへの依存度:ブラウザの S(t) が短いなら、実効的なキャッシュはCDNのエッジサーバー(ユーザーの近くに配置され、CDNのキャッシュを保持するサーバー)で担保するしかありません
ファイルの統合と削減が効くのは、ChromiumとFirefoxのソースコードで見たとおり、追い出しの基準にファイルのサイズや利用頻度が含まれているからです。
この点は、WebKitの元になったKHTML以来の設計でもあり、その系譜も上記の記事で扱っています。
まとめ
- max-ageは「問い合わせなしで使ってよい期間」の宣言であって、保存期間ではありません。長いmax-ageは性能上の期待値が低く、かつ内容が変わり得るURLには危険です
- 最適なmax-ageは、「内容が変わったときにURLも変わるか」で決まります。URLが変わるリソースは1年、変わらないリソースは更新の許容時間で決めます
- max-ageは追い出しの判断に使われません。ChromiumとFirefoxのソースコードで確認できます。短くしても容量は空きません。長くしても害がないのはバージョン付きURLに限り、同じURLで内容を差し替えるリソースには長いmax-ageは危険です
- ストレージの制約が効くのは、max-ageの値ではなく、生存関数S(t)の形です。S(t)が十分小さくなる時刻が、それ以上長くしても意味がないmax-ageの上限です
- Chromiumは「経過時間×サイズ」、Firefoxは「利用頻度」で追い出します。大きいもの、使われないものから消える点は共通で、ファイルの統合と削減がキャッシュの生存に効きます
- ストレージの制約に対して本当に効く設計変数は、サイトの総リソース量と、CDNへの依存度です
- バージョン付きのファイル名のまま中身を上書きすると、CDNのPurgeでは直せない事故になります。ファイル名の変更を、ビルドの仕組みか運用手順で強制してください
この記事が事業に意味すること
max-ageの値を誤ると、2種類の事故が起きます。
長すぎれば、キャンペーン価格や注意事項を更新しても古い表示が残り続けます。
短すぎれば、CDNが本来肩代わりするはずのリクエストがオリジンサーバー(CDNの背後にある、Webサイト本来のサーバー)に届き、サーバー費用と表示速度の両方に跳ね返ります。
どちらも「設定した本人は正しくやったつもり」で起きるため、事故が起きるまで気づきません。
弊社は、CDN(Gcore)の導入と合わせて、リソースの種類ごとのキャッシュ設計、CDNのキャッシュ設定、更新時の反映手順までを設計しています。
CDNは、契約するだけでは速くならず、キャッシュ設計が先です。
CDNの導入や見直しを検討している場合は、設計の段階でお問い合わせフォームからご相談ください。
どのような改善で、どれだけ速くなったかは、高速化成功事例をご覧ください。
参考資料
- RFC 9111: HTTP Caching
- RFC 8246: HTTP Immutable Responses
- Prevent unnecessary network requests with the HTTP Cache(web.dev)
- Fix Search-related JavaScript problems(Google検索セントラル)
- Chromium: net/disk_cache/cache_util.cc
- Chromium: net/disk_cache/simple/simple_index.cc
- Firefox: netwerk/cache2/CacheIndex.cpp(GitHubミラー)
- Learn about Akamai's caching(Akamai TechDocs)
- Origin Cache Control(Cloudflare Docs)
- Cache freshness and TTLs(Fastly Documentation)
- Manage how long content stays in the cache(Amazon CloudFront Developer Guide)
- CDN cache lifetime configuration(Gcore Docs)
- ブラウザキャッシュは何日残るのか(PerfData)
- キャッシュの寿命はなぜ確率で扱うのか(PerfData)
- ブラウザキャッシュのヒット率を生存時間解析で実測する(PerfData)