HTTPキャッシュヘッダー入門|Cache-Control・ETag・Last-Modifiedの設計と実装

kento_morota 14分で読めます
社内のAI・IT活用、技術がわかる相手に無料相談。 相談 →
目次

「デプロイしたのに一部のユーザーには古いJavaScriptが表示される」「no-cacheを付けたのにキャッシュされているように見える」「CDNを入れたら会員ページの内容が他人に見えた」。HTTPキャッシュのトラブルは、ヘッダーの意味を“なんとなく”で設定していると必ず一度は踏みます。逆に正しく設計できれば、サーバー負荷と表示速度の両方を大きく改善できる、費用対効果の高い領域です。

この記事では、ブラウザのキャッシュとCDNなどの共有キャッシュがどう違うかから始め、Cache-Controlの各ディレクティブの正確な意味、ETagLast-Modifiedを使った条件付きリクエストと304レスポンスの仕組み、ハッシュ付きファイル名を使った「長期キャッシュしても更新が反映される」戦略、そしてNginx・Express・Astroでの具体的な設定例を扱います。CDNそのものの仕組みはCDNとは?仕組みとメリットに譲り、本記事は「ヘッダーの設計」に集中します。

ブラウザキャッシュと共有キャッシュの違い

キャッシュは1か所ではなく、経路上に何段もある

レスポンスがユーザーに届くまでの間には、複数のキャッシュが存在し得ます。ユーザーのブラウザが持つ「プライベートキャッシュ」は、そのユーザー1人のためのものです。一方、CDNのエッジサーバー、リバースプロキシ、企業のプロキシサーバーなどが持つ「共有キャッシュ」は、複数のユーザーで同じレスポンスを共有します。この区別が重要なのは、個人向けの内容(ログイン後のマイページなど)が共有キャッシュに入ると、他のユーザーに配られてしまうからです。

Cache-Controlヘッダーは、この2種類のキャッシュに対して「保存してよいか」「どれくらい新鮮とみなしてよいか」「再利用前に確認が必要か」を指示するものです。指示を書かなかった場合、ブラウザはLast-Modifiedから経過時間の10%程度を新鮮とみなす「ヒューリスティックキャッシュ」を行うことがあり、「何も設定していないのにキャッシュされる」現象の正体はこれです。キャッシュしたくないものには、明示的に指示を書く必要があります。

Cache-Controlディレクティブの正確な意味

よく使うディレクティブ一覧

ディレクティブ意味対象
max-age=秒取得からこの秒数は「新鮮」とみなし、サーバーに問い合わせず再利用してよいブラウザ・共有
s-maxage=秒共有キャッシュだけに適用される新鮮期間。指定があれば共有キャッシュではmax-ageより優先共有のみ
public共有キャッシュに保存してよい(認証付きレスポンスでも保存可能にする)共有
privateブラウザにだけ保存してよく、共有キャッシュには保存させない共有を禁止
no-cache保存はしてよいが、再利用する前に必ずサーバーへ検証(条件付きリクエスト)するブラウザ・共有
no-store一切保存しない。個人情報・決済画面などブラウザ・共有
must-revalidate新鮮期間が切れたら、古いものを使わず必ず検証する(通常は切れても状況次第で使うことがある)ブラウザ・共有
immutable新鮮期間中は、ユーザーがリロードしても検証すらしない。内容が変わらないファイル用ブラウザ
stale-while-revalidate=秒新鮮期間が切れた後もこの秒数は古い内容を即座に返し、裏で更新を取りに行くブラウザ・共有
stale-if-error=秒オリジンがエラーを返したとき、この秒数は古い内容で代替する主に共有

誤解されやすいのはno-cacheです。名前に反して「キャッシュしない」ではなく「毎回確認してから使う」という意味で、内容が変わっていなければ304で済むため通信量は減ります。本当に保存させたくないならno-storeです。もう1つはmax-age=0no-cacheの関係で、実務上ほぼ同じ挙動になりますが、意図を明確にするならno-cacheを使います。

典型的なリソース種別ごとの設定

リソース推奨ヘッダー理由
ハッシュ付きJS/CSS/画像(app.3f2a9c.jspublic, max-age=31536000, immutable内容が変わればファイル名も変わるので、1年保存して問題ない
HTML(エントリーポイント)no-cache または max-age=0, must-revalidate常に最新のファイル名一覧を参照させる必要がある
公開API(全員同じ内容)public, s-maxage=60, stale-while-revalidate=300CDNで60秒共有し、期限切れ後も即応答しつつ裏で更新
ログイン後のページ・個人データprivate, no-store共有キャッシュに入ると情報漏えい。ブラウザにも残さない
ファイル名が固定の画像(ロゴなど)public, max-age=86400+ETag1日は再利用、以降は304で確認

条件付きリクエストと304:ETagとLast-Modified

「変わっていなければ本文を送らない」仕組み

新鮮期間が切れたキャッシュやno-cacheのリソースを再利用する前、ブラウザは「前回もらったものから変わっていますか」とサーバーに確認します。これが条件付きリクエストです。サーバーが前回のレスポンスに付けていたETag(内容を識別するトークン)をIf-None-Matchヘッダーで、またはLast-Modified(最終更新日時)をIf-Modified-Sinceヘッダーで送り返し、サーバーは変わっていなければ本文なしの304 Not Modifiedを返します。ブラウザは手元のキャッシュをそのまま使い、新鮮期間をリセットします。

ETagにはファイルの内容ハッシュなどを使うため、更新日時が変わっても内容が同じなら304にできる点でLast-Modifiedより正確です。Nginxは静的ファイルに対してデフォルトでETagを生成し(ファイルの更新時刻とサイズから算出)、gzipで動的圧縮する場合は先頭にW/を付けた弱いETagに変えます。弱いETagは「意味的には同じ」を表し、304の判定には使えます。

# 1回目:通常のレスポンス
$ curl -sI https://example.com/logo.png
HTTP/2 200
etag: "66d0a1f2-3c8a"
last-modified: Thu, 29 Aug 2026 10:15:30 GMT
cache-control: public, max-age=86400

# 2回目:ETagを添えて問い合わせると、変更がなければ304で本文なし
$ curl -sI -H 'If-None-Match: "66d0a1f2-3c8a"' https://example.com/logo.png
HTTP/2 304
etag: "66d0a1f2-3c8a"
cache-control: public, max-age=86400

注意点として、304は「通信は発生している」点です。数百のファイルをそれぞれ304で確認するのはラウンドトリップの無駄なので、頻繁に読み込まれる静的アセットは次に述べるハッシュ付きファイル名で「確認すらしない」状態にするのが理想です。

ハッシュ付きファイル名戦略:長期キャッシュと即時更新を両立する

内容が変わればURLも変わる、だから1年キャッシュできる

「長期間キャッシュしたいが、更新は即座に反映したい」という矛盾する要求を解決するのが、ファイル名に内容のハッシュを埋め込む方法です。ビルドツール(Vite、webpack、Astroなど)はapp.jsapp.3f2a9c1b.jsのように出力し、HTMLからそのファイル名で参照します。内容を変えればハッシュが変わり、別のURLになるため、古いキャッシュが使われることはありません。したがって、これらのファイルにはmax-age=31536000, immutable(1年、検証不要)を付けて構いません。

一方で、ハッシュ付きファイル名を参照している側のHTMLは、常に最新のファイル名を知っている必要があります。HTMLを長期キャッシュすると「古いHTMLが古いJSを参照し続ける」ことになるため、HTMLはno-cacheにして毎回検証させます。「HTMLは短く、アセットは長く」がこの戦略の要点で、ほとんどのモダンなフロントエンド構成はこの前提で作られています。パフォーマンス改善の全体像はWebパフォーマンス最適化入門を参照してください。

Nginx・Express・Astroでの設定例

Nginx

server {
    root /var/www/site;

    # ビルド生成物(ハッシュ付き)は1年・immutable
    location ~* ^/(_astro|assets)/.+\.(js|css|woff2|png|jpg|svg)$ {
        add_header Cache-Control "public, max-age=31536000, immutable";
        access_log off;
    }

    # HTMLは毎回検証(ETagはデフォルトで有効)
    location ~* \.html$ {
        add_header Cache-Control "no-cache";
    }

    # ファイル名が固定の画像は1日
    location ~* \.(ico|png|jpg|jpeg|gif|webp)$ {
        add_header Cache-Control "public, max-age=86400";
    }

    location / {
        try_files $uri $uri/ /index.html;
        add_header Cache-Control "no-cache";
    }
}

add_headerは、locationブロック内に1つでも書くと上位(serverブロック)のadd_headerを継承しなくなります。セキュリティヘッダーなどをserverブロックに書いている場合、キャッシュ用locationにも再掲する必要がある点に注意してください。

Express(Node.js)

const express = require('express');
const app = express();

// ハッシュ付きアセット:1年・immutable
app.use('/assets', express.static('dist/assets', {
  maxAge: '1y',
  immutable: true,
  etag: false,          // 検証不要なのでETagも省略可
}));

// その他の静的ファイル:ETag/Last-Modified付きで1時間
app.use(express.static('public', { maxAge: '1h' }));

// 公開API:CDNで60秒共有、期限切れ後5分は古い内容で即応答
app.get('/api/news', (req, res) => {
  res.set('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300');
  res.json(getNews());
});

// 個人データ:保存禁止
app.get('/api/me', requireAuth, (req, res) => {
  res.set('Cache-Control', 'private, no-store');
  res.json(req.user);
});

Expressは動的レスポンスにもデフォルトでETag(本文のハッシュ)を付けます。大きなJSONを毎回ハッシュ計算するコストが気になる場合はapp.set('etag', false)で無効化できます。Expressの基本はExpress.jsでREST API構築入門で扱っています。

Astro

Astroの静的ビルドでは、ハッシュ付きアセットは_astro/ディレクトリに出力されます。静的サイトの場合、ヘッダーはアプリではなくホスティング側(Nginx、Vercel、Netlify、CloudFrontなど)で設定します。上のNginx例の_astroがそれに対応し、Vercelならvercel.jsonheadersで同じ内容を宣言します。SSRモードで動的ページを返す場合は、ページやエンドポイントの中でヘッダーを設定できます。

---
// src/pages/news/[id].astro(SSR)
const news = await getNews(Astro.params.id);
Astro.response.headers.set(
  'Cache-Control',
  'public, s-maxage=300, stale-while-revalidate=3600'
);
---
<h1>{news.title}</h1>

Astroを採用する利点や静的配信の考え方はAstro.js導入のメリットにまとめています。

トラブル事例:デプロイ後も古い画面が表示され続ける

症状

SPAをリリースしたところ、一部のユーザーからは新機能が見えず、開発者ツールを見るとJSファイルの読み込みで404が出ている。管理者のブラウザでスーパーリロードすると直るが、ユーザー全員にそれを頼むわけにはいかない。

原因

Nginxでlocation / { add_header Cache-Control "public, max-age=31536000"; }と、すべてのパスに一律で1年のキャッシュを付けていた。index.htmlまでブラウザに1年保存されたため、古いHTMLが古いハッシュ名のJS(デプロイで削除済み)を参照し続け、404になっていた。「アセットを長期キャッシュする」という意図は正しかったが、HTMLを除外していなかったことが問題だった。

対処

HTMLにno-cacheを返すようlocationを分離し、ハッシュ付きアセットだけにimmutableを付けた。既にキャッシュしてしまったユーザーには効かないため、暫定的に旧ハッシュのファイルを一定期間残す(ビルド生成物を削除せず追加のみで配置する)運用にし、1年のキャッシュ期限が来る前に自然に解消されるのを待つ形をとった。あわせて、リリース手順に「シークレットウィンドウで初回アクセスし、HTMLのCache-Controlを確認する」項目を追加した。

まとめ

HTTPキャッシュは、ブラウザのプライベートキャッシュとCDNなどの共有キャッシュの2層があり、Cache-Controlでそれぞれに保存可否と新鮮期間を指示します。no-cacheは「検証してから使う」、no-storeは「保存しない」、s-maxageは共有キャッシュ専用、immutableは「検証すらしない」と、意味を正確に押さえてください。期限切れ後はETagLast-Modifiedによる条件付きリクエストで304を返し、頻繁に使うアセットはハッシュ付きファイル名で1年キャッシュ、それを参照するHTMLはno-cacheにするのが基本戦略です。個人向けの内容には必ずprivate, no-storeを付け、設定後はcurlで実際のヘッダーを確認する習慣をつけましょう。Nginx全体の性能設定はNginxパフォーマンスチューニングで扱っています。

キャッシュ戦略やCDN構成の設計でお困りの際は、Harmonic Societyのシステム開発・インフラ支援にご相談ください。

#HTTPキャッシュ#Cache-Control#ETag#パフォーマンス

Harmonic Society

この記事の内容、自社の業務でも活かせそうですか?

ローカルLLM・AI・クラウドなどの技術導入を、要件整理からPoC・社内展開まで代表エンジニアが伴走します。オンライン対応・全国OK。まずは30分の無料相談から。売り込みはしません。

共有:
無料メルマガ

週1回、最新の技術記事をお届け

AI・クラウド・開発の最新記事を毎週月曜にメールでお届けします。登録は無料、いつでも解除できます。

プライバシーポリシーに基づき管理します

関連記事

Related / 9 articles

  1. プログラミング

    DDoS攻撃の仕組みと対策入門|レイヤー別の防御とCDN・クラウドの活用

    DDoS攻撃をボリューム型・プロトコル型・アプリ層に分けて仕組みを解説し、自前サーバーで防げない理由、CloudflareやAWS Shieldの標準防御、オリジンIPの隠し方、レートリミットとBot対策、攻撃を受けたときの初動、費用が跳ね上がるDenial of Walletへの備えまでわかります。

  2. プログラミング

    WAFとは?仕組み・導入パターン・誤検知対策|Webアプリを攻撃から守る実践ガイド

    WAFがファイアウォールやIDSと何が違うのか、シグネチャとマネージドルールの仕組み、Cloudflare WAF・AWS WAF・ModSecurityの比較、フォーム送信がブロックされる誤検知の調査と例外設定、ログ監視、WAFが代替できないことまで実践的に解説します。

  3. プログラミング

    セキュリティヘッダー入門|CSP・HSTS・X-Frame-Optionsの設定と効果を実践解説

    CSP・HSTS・X-Frame-Options・X-Content-Type-Optionsなど主要セキュリティヘッダーが防ぐ攻撃と、CSPのReport-Onlyからの段階導入、nonce/hash、HSTS preloadの不可逆リスク、Nginx・Next.js・Astroでの設定例、確認方法を解説します。

  4. プログラミング

    クラウドの通信費(Egress)入門|データ転送量課金の仕組みと転送コストを抑える設計

    クラウドの「受信無料・送信有料」の原則、AZ間・リージョン間・インターネット向けの単価差、NATゲートウェイ処理料の罠、CDNで転送量を減らす方法、バックアップやログ転送の見落とし、請求書で転送料を特定する手順を解説。想定外の請求を防げます。

  5. プログラミング

    秘密情報をGitに入れない仕組み|.gitignore・git-secrets・履歴から漏れた鍵の削除

    APIキーや.envをGitにコミットしてしまう典型経路と、.gitignore・.env.exampleの運用、pre-commitでのgitleaks検知、GitHub secret scanningの活用、漏れた鍵の無効化と履歴書き換え(git filter-repo)の手順を解説。仕組みで再発を防げます。

  6. プログラミング

    開発・ステージング・本番環境の分離設計|環境差分をなくす構成とアクセス制御

    開発・ステージング・本番それぞれの目的と、構成をコードで揃える方法、環境別の設定注入、本番データを使わないテストデータ戦略、ステージングの保護(Basic認証・IP制限・noindex)、コストを抑える運用までを解説。環境差分による本番障害を防げます。

  7. プログラミング

    ngrok・Cloudflare Tunnelでローカルを公開|Webhook開発とデモ環境の作り方

    NAT内のローカル環境にStripeやLINEのWebhookを届けるトンネリングの仕組みを解説。ngrok・Cloudflare Tunnel・localtunnelの比較、固定ドメインと認証、リクエスト検査、公開時のセキュリティ、自宅サーバー公開への応用までわかります。

  8. プログラミング

    ローカル開発環境のHTTPS化|mkcert・hostsファイル・自己署名証明書の正しい使い方

    ローカル開発をHTTPS前提にすべき理由(Secure Cookie・Service Worker・OAuth)と、mkcertでローカルCAを作りhostsで独自ドメインを割り当ててVite・Next.js・Dockerで使う手順を解説。証明書警告を無視する癖の危険も理解できます。

  9. プログラミング

    localhost・0.0.0.0・127.0.0.1の違い|ポートとUnixソケットを理解して「つながらない」を解決

    localhost・127.0.0.1・0.0.0.0の意味の違い、Dockerで外から接続できない原因、host.docker.internal、ポート競合の調べ方、Unixソケットの利点と権限、1024未満ポートの制約を解説。「つながらない」を仕組みから解決できます。

Harmonic Society

「読んで終わり」にせず、自社の業務で試してみませんか?

AI・ローカルLLM・クラウドの導入を、要件整理からPoC・社内展開まで代表エンジニアが伴走します。オンライン対応・全国OK・売り込みなし。

無料・30分・オンラインOK|1営業日以内に返信します