Tech Learning Daily

2026-09-21 (Mon) — 第 66 号
AI が毎朝届ける、ソフトウェア技術の基礎解説
Network 🌿 基礎 ⏱ 約 6 分

レスポンスは 200 のはずなのに、なぜ通信量が 0 バイトになるのか?

Chrome の DevTools で Network タブを開くと、たまに Status が 304 で Size がほぼ 0 の行を見かける。Cache-ControlETag というヘッダー名は知っていても、それが「キャッシュを使うか使わないか」の二択ではなく、2 段階の判断でできていることまで説明できるだろうか。

🎯 3 行まとめ
  • HTTP キャッシュは「サーバーに聞かずに使ってよい期間(鮮度)」と「聞いてから使う判断(検証)」の 2 段構えでできている。
  • no-cache は「キャッシュするな」ではなく「毎回サーバーに確認してから使え」という意味で、これを誤解すると意図しない挙動になる。
  • 検証リクエストが変更なしと分かると、サーバーは本文を送り返さず 304 Not Modified だけを返す。これが通信量 0 の正体である。

キャッシュには「鮮度」と「検証」という 2 つの層がある

Web アプリを書いていれば Cache-Control: max-age=3600 のようなヘッダーは見たことがあるはずだ。これは「レスポンスがどれだけの秒数、鮮度(fresh)を保つか」を示す値で、ブラウザはこの期間中はサーバーに一切問い合わせず、保存済みのレスポンスをそのまま使う。ここまでは直感通りだろう。

問題は鮮度が切れて古く(stale)なった後だ。キャッシュを丸ごと捨てて再取得するしかないように思える。しかし実際は、本文はまだ使えるかもしれないので、サーバーに「これ、まだ変わってない?」とだけ確認する検証(validation)という別の仕組みが用意されている。鮮度と検証は別レイヤーであり、この 2 段構えを分けて理解しないと、なぜ no-cacheno-store がまったく違う指示なのかが分からなくなる。

🍱 たとえるなら

鮮度は「賞味期限まではパッケージを開けずに棚に置いておいてよい」であり、検証は「賞味期限は切れたが、店員に電話して『まだ入荷分と同じロットで大丈夫か』を確認してから棚に戻す」動作に近い。期限切れ=即廃棄ではなく、電話 1 本(=軽いリクエスト)で済むなら中身(=レスポンスボディ)を運び直す必要はない。

no-cache は「キャッシュするな」ではない

MDN の Cache-Control リファレンスが明確に注意しているように、no-cache保存自体は許可しつつ、再利用の前に必ずサーバーへ検証リクエストを送らせる指示である。名前から「キャッシュ禁止」と誤解しがちだが、保存を完全に禁止するのは no-store の役目だ。この 2 つを混同すると、「no-cache を付けたのに Network タブにキャッシュされたエントリが残っている」ように見えて混乱する。

Cache-Control: no-cache          # 保存はする。使う前に必ず確認する
Cache-Control: no-store          # 保存自体を禁止する(個人情報など)
Cache-Control: max-age=3600      # 3600 秒はサーバーに聞かずに使う
Cache-Control: public, max-age=31536000, immutable  # CDN含め共有可・1年間・検証も不要

immutable を付けると、鮮度が切れていなくても発生しうる「リロード時の強制検証」まで省略できる。ハッシュ付きファイル名(bundle.abc123.js)のように内容が変わればURL自体が変わる静的アセットに向く指示で、HTML のような同一 URL を使い回すリソースには使えない。

検証の実体は ETagIf-None-Match の往復

検証には主に 2 つの方式がある。Last-Modified/If-Modified-Since は更新日時(秒単位)で比較する古い方式、ETag/If-None-Match はサーバーが生成したハッシュ値などの識別子で比較する方式だ。両方が使える場合、If-None-Match による比較が優先される。時刻の分解能では見分けられない短時間の更新も検出できるためである。

1回目のリクエスト
 client                    server
   │  GET /style.css          │
   │─────────────────────────▶│
   │  200 OK                  │
   │  ETag: "33a64df5"        │
   │◀─────────────────────────│
   │ (本文を保存)              │

鮮度切れ後、2回目のリクエスト
   │  GET /style.css          │
   │  If-None-Match:"33a64df5"│
   │─────────────────────────▶│
   │  304 Not Modified        │
   │  (本文なし)               │
   │◀─────────────────────────│
   │ (保存済みの本文を再利用)   │

サーバー側は受け取った If-None-Match の値と現在の ETag を比較し、一致すれば本文を作り直さず 304 Not Modified だけを返す。ヘッダーだけの応答になるため、DevTools 上では Status 304・Size がほぼ 0 バイトのリクエストとして観測できる。これは「キャッシュを使わなかった」のではなく、「キャッシュを使ってよいと確認が取れた」結果である。

HTML と静的アセットで戦略を変える理由

静的アセット(バンドル済み JS/CSS/画像)はファイル名にハッシュを含めれば URL 自体がコンテンツを表すため、public, max-age=31536000, immutable のように長期間かつ無条件でキャッシュしてよい。内容が変わればビルドが別 URL を生成するので、古いキャッシュが混ざる心配がない。

一方 HTML は URL を変えられない(/index.html は常に同じアドレス)ため、長い max-age を付けると更新が届かなくなる。そこで Cache-Control: no-cache を付け、ETag による軽量な検証だけは毎回行う、という設計が一般的になる。ユーザーごとに内容が変わるページ(ログイン後の画面など)では、共有キャッシュに乗らないよう private も併記する。

💼 実務でどう出会うか

API サーバーのレスポンスが妙に古い値を返し続ける不具合を追うと、原因がリバースプロキシや CDN の Cache-Control 解釈にたどり着くことがある。CDN 向けには s-maxage で共有キャッシュ専用の秒数を指定でき、これはブラウザ側の max-age より優先される。フロントエンドのビルド設定でアセットに immutable を付け忘れると、デプロイのたびに全ユーザーが同じ変わらないファイルを再検証しにきて、無駄なリクエストが積み上がることもある。

⌨️ 手を動かす(5 分)

公開サイトの静的ファイルに対して、キャッシュヘッダーと検証の往復を実際に観察する。

# 1回目: レスポンスヘッダーを確認(ETag を控える)
curl -sI https://developer.mozilla.org/favicon.ico

# 2回目: 控えた ETag を If-None-Match に渡して検証リクエストを送る
curl -sI https://developer.mozilla.org/favicon.ico \
  -H 'If-None-Match: "<1回目に控えたETagの値をここに>"'

ETag が一致していれば 2 回目は HTTP/2 304 が返り、Content-Length がヘッダーに現れないか極端に小さいことが確認できるはずだ。値を変えて試すと 200 OK に戻る違いも見える。

🙅 よくある誤解
  • no-cache を付ければキャッシュされない — 保存はされる。次回利用前にサーバーへ確認が入るだけで、保存自体を止めるのは no-store である。
  • 304 は「キャッシュが効かなかった失敗」 — 304 は検証が成功し、本文の再送を省略できたことを示す成功応答である。
  • ETag があれば Cache-Control は不要Cache-Control は「そもそもサーバーに聞くかどうか」を決め、ETag は「聞いた時に何と比較するか」を決める。役割が異なるため両方が必要になる場面が多い。
📖 用語ミニ辞典
鮮度 (freshness)
レスポンスがサーバーに問い合わせずに再利用できる期間。max-age で秒数を指定する。
検証 (validation)
鮮度切れ後、内容が変わっていないかをサーバーに確認する仕組み。ETag や Last-Modified を使う。
ETag
サーバーがレスポンスに付与する内容識別子。内容が変われば値も変わる。
If-None-Match
クライアントが手持ちの ETag をサーバーに送り、一致するか問い合わせるリクエストヘッダー。
304 Not Modified
検証の結果、内容が変わっていないことを示すステータスコード。本文を含まない。
s-maxage
CDN・プロキシなど共有キャッシュにのみ適用される鮮度秒数。ブラウザの max-age より優先される。
🔗 もっと深く
最近の記事

負荷が落ち着いたのに、なぜ HPA はしばらく Pod を減らしてくれないのか?

HPAは既定15秒間隔の制御ループでdesiredReplicas=ceil(現在数×実測/目標)を計算するだけの仕組み。既定10%のトレランスでフラッピングを防ぎ、scaleUpは0秒・scaleDownは既定300秒のstabilizationWindowSecondsという非対称設計で急な縮小を避ける理由を解説。
Container🌿 基礎⏱ 約 7 分2026-09-20

Kafka のコンシューマーを増やしたのに、なぜ処理速度は頭打ちになるのか?

Kafkaのパーティションはグループ内で同時に1コンシューマーにしか割り当てられず並列度の天井になる。パーティション数を減らせない設計、Eager/Cooperativeリバランスの違い、オートコミットの既定5秒間隔が重複処理を生む仕組みを解説。
Arch🌳 応用⏱ 約 8 分2026-09-19

EC2 はパスワードを一度も入力していないのに、なぜ S3 にアクセスできるのか?

IAMロールは信頼ポリシー(誰が借りるか)と許可ポリシー(何ができるか)の二階建てで、STSのAssumeRoleが既定1時間・最大12時間の一時クレデンシャルを発行する仕組み。EC2でのIMDS経由の自動更新と、ロール剥奪が最大1時間遅延する挙動を解説。
Infra🌿 基礎⏱ 約 7 分2026-09-18

同じ「テーブルを割る」なのに、パーティショニングとシャーディングは何が違うのか?

パーティショニングは1台のDB内でのテーブル分割、シャーディングは複数サーバーへの分散という違いを解説。partition pruningの仕組み、shard keyを跨ぐJOIN・トランザクションの壁、リシャーディングの難しさを扱う。
DB🌳 応用⏱ 約 7 分2026-09-17