コードを変更してデプロイしたのに、ブラウザでは古いCSSが表示される。DBの値は更新されているのに、APIから以前の値が返ってくる。DNSレコードを切り替えたのに、一部の環境だけ旧サーバーへ接続される。

どれも見た目は「変更したのに反映されない」という問題だが、古い状態が残っている場所はそれぞれ違う。

前の記事「Webのキャッシュはどこにある? ブラウザからDBまで、置き場所と目的で全体像を見る」では、クライアント、CDN、アプリケーション、DB、OS・ストレージなど、Webサービスの複数の場所にキャッシュが存在することを見た。この記事では、その経路を実際のトラブル調査に当てはめていく。

「変更した場所」と「参照している場所」は同じとは限らない

Origin上のJavaScriptを更新しても、ブラウザが以前のレスポンスを再利用していれば新しいファイルは取得されない。DBを更新しても、APIがRedisに残っている値を返せばDBまで問い合わせは届かない。

変更元とユーザーの間には複数の参照先がある

調査するときは、最初に次の2点を分けて見ると追いやすい。

  1. 変更元は本当に新しい状態になっているか
  2. ユーザーが実際にどの層から値を受け取っているか

原因が分からないままブラウザ、CDN、Redisのキャッシュをまとめて消すと、たしかに直ることはある。ただ、どこに古い値が残っていたのかは分からなくなる。同じ症状が再発すれば、また同じ範囲を調べ直すことになる。

最初に変更元を確認する

キャッシュを追う前に、変更そのものが本番へ届いているかを確認しておきたい。ここを飛ばすと、デプロイ失敗や接続先の取り違えまでキャッシュの問題として追いかけることになる。

たとえば次のような点を見る。

  • デプロイ先に新しいファイルがあるか
  • 実際に新しいコミットがデプロイされているか
  • DBの対象レコードが更新されているか
  • DNSの権威サーバーが新しいレコードを返しているか
  • 本番環境とは別の環境を見ていないか

変更元そのものが古ければ、先にデプロイや更新処理を調べる。変更元は新しいのにユーザー側だけ古いなら、その間にあるキャッシュを追っていけばよい。

どこから返ってきたレスポンスなのかを見る

WebページやAPIでは、ブラウザのNetworkパネルや curl でレスポンスヘッダーを見ると、どの層を疑うべきか判断しやすくなる。

curl -I https://example.com/app.js

代表的な項目は次の通り。

項目見えること
Cache-Controlどの程度再利用できるレスポンスとして扱われているか
Age共有キャッシュが保持しているレスポンスの年齢を示す場合がある
ETag再検証で同一コンテンツか判定するための識別子
Last-Modifiedリソースの更新日時を使った再検証に利用される
CDN固有のヘッダーHIT、MISS、BYPASSなど、そのCDNでのキャッシュ状況が分かる場合がある

もちろん、レスポンスヘッダーだけですべて分かるわけではない。SWRやTanStack Queryのキャッシュ、Redis、プロセス内メモリなどは、HTTPレスポンスだけを見ても直接は分からない。

それでも「ブラウザから通信が出ているか」「CDNでHITしているか」と順に確認すれば、古い値を作っている場所はかなり絞れる。

CSSやJavaScriptを変更したのに見た目が変わらない

静的ファイルでは、ブラウザやCDNに以前のファイルが残っているケースが多い。

たとえばHTMLが次のファイルを読み込んでいるとする。

<script src="/assets/app.js"></script>

app.js の内容だけを差し替え、長いキャッシュ期限を設定していると、クライアントは以前取得したファイルをそのまま使うことがある。

強制再読み込みで直るならブラウザ側を疑いやすい

開発者ツールでキャッシュを無効化した状態や強制再読み込みで新しい表示になるなら、ブラウザ側のHTTPキャッシュが関係している可能性が高い。

強制再読み込みは原因を探すには便利だが、利用者全員に毎回やってもらうわけにはいかない。静的アセットでは、内容が変わったときにURLも変わるようにしておく方法がよく使われる。

app.4d3f1a.js
app.a82c90.js

ファイル名にcontent hashなどを含めれば、新しいデプロイでは別URLとして取得される。古いファイルには長いキャッシュ期限を設定したまま、新しいHTMLから新しいURLへ切り替えられる。

HTMLだけ古いケースもある

JavaScriptのファイル名をhash化していても、それを参照するHTML自体が古ければ、新しいファイルへ切り替わらない。

古いHTML

app.4d3f1a.js を参照

新しいHTML

app.a82c90.js を参照

JSファイルそのものに問題が見つからないときは、入口になるHTMLやSSR結果が古いまま残っていないかも確認したい。

再読み込みしても直らないならService Workerも確認する

Service Workerを使うサイトでは、通常のHTTPキャッシュだけを見ても原因が見つからないことがある。Service Worker自身がリクエストを受け取り、Cache Storageに保存してあるレスポンスを返せるためだ。

Service Workerは通常のネットワークアクセスより手前で応答できる

Service Workerを使っているなら、次のあたりを確認する。

  • 新しいService Workerがインストールされているか
  • 古いService Workerがまだ制御していないか
  • Cache Storageに古いファイルが残っていないか
  • キャッシュ名やバージョンを更新しているか
  • cache-first などの戦略で古い値を優先し続けていないか

開発中にService Workerを解除したら直ることもあるが、それだけでは更新時に同じ問題が起きる。新しいService Workerへ切り替わるタイミングや、古いキャッシュを削除する処理まで見ておく必要がある。

CDNをPurgeしたのに古いままなら、その手前も見る

CDNのキャッシュを削除すれば、次にCDNへリクエストが届いたときはOriginから新しいレスポンスを取得できる。ただし、ブラウザに残っているキャッシュまで同時に消えるわけではない。

Browser Cache

CDN Cache  ← purge済み

Origin

ブラウザがローカルのキャッシュを使っていてCDNへリクエストを送っていないなら、CDNをPurgeしても表示は変わらない。

ブラウザから新しいリクエストが出ているのにCDNから古い内容が返るなら、CDN側の設定やCache Keyを確認する。

  • 対象URLを正しくPurgeしたか
  • Query Stringを含めたCache Keyになっているか
  • HostやHeaderでCache Keyが分かれていないか
  • 別のEdgeや上位キャッシュに古い値が残っていないか
  • stale responseを返す設定になっていないか

ブラウザからは同じパスに見えても、CDNではQuery StringやHost、Headerを含めて別のCache Keyとして扱っている場合がある。Query Stringを無視する設定なら、?v=2 を付けても別キャッシュにはならない。

APIが古いときは、リクエストが出ているかを見る

APIレスポンスが古く見えるとき、ブラウザのHTTP Cacheより手前で、アプリケーション側のデータキャッシュが値を返していることもある。

SWRやTanStack Queryなどは、取得したAPIデータをクライアント側で保持する。

UI

Client Data Cache

fetch / API

更新APIは成功しているのに一覧画面だけ古い、といったケースでは、更新後に対象queryをinvalidateしていなかったり、別のquery keyに古い値が残っていたりする。

Networkパネルを開き、画面を開き直したときにAPIリクエストが発生しているかを見ると切り分けやすい。リクエスト自体が出ていなければ、サーバーまで追う前にクライアント側の状態を確認できる。

リクエストが発生していて、そのレスポンス本文がすでに古い場合はサーバー側へ進む。

DBは新しいのにAPIが古いならApplication Cacheを確認する

サーバー側では、Redisやプロセス内メモリが古い値を返していることがある。

API Request

Application

Redis HIT → 古い値
  ↓ MISS
Database → 新しい値

この状態では、DBを直接見ると新しい値なのに、API経由では古い値が返る。

調べるときは、実際に参照しているキャッシュキー、TTL、更新時のinvalidate処理などを確認する。

  • 実際に参照しているキャッシュキー
  • TTL
  • 更新時にinvalidateしているか
  • 書き込み後に同じキーを再生成しているか
  • 複数インスタンスでプロセス内キャッシュが分かれていないか

プロセス内メモリを使っている場合は、複数インスタンス構成にも注意したい。

Instance A → 新しいキャッシュ
Instance B → 古いキャッシュ

ロードバランサーの振り分けによって新旧が交互に見えるなら、1台だけのキャッシュを見ても原因は分からない。

Redisでも同じで、調査中に全キーを消してしまうと手掛かりも一緒に消える。まず対象キーと値を確認し、必要な範囲だけ無効化した方が追いやすい。

DNSレコードを変更したのに旧IPへ接続される

DNSも「変更したのに反映されない」と感じやすい場所の一つだ。ただし、HTTPキャッシュとは別の経路で動いている。

権威DNSが更新済みでも途中のResolverが以前の値を保持している場合がある

権威DNSでレコードを書き換えても、Recursive Resolverなどが以前の値をTTLの範囲で保持していれば、その環境では旧IPが返る。

「DNSの変更が世界中へ少しずつ伝播している」と説明されることも多いが、実際には各Resolverに残っている旧レコードの期限が切れる時刻が揃っていないことも大きい。どこで古い値が返っているのかを見るには、Resolverごとの結果を比べる。

Resolverごとの差を見る

dig が使える環境なら、通常の名前解決結果と特定のRecursive Resolverを比較できる。

dig example.com A

dig @1.1.1.1 example.com A

dig @8.8.8.8 example.com A

権威DNS側を確認する場合は、まずNSを調べる。

dig example.com NS

得られた権威DNSサーバーへ直接問い合わせる。

dig @ns1.example.net example.com A +norecurse

権威DNSは新しいIPを返し、Recursive Resolverだけが古いIPを返すなら、そのResolverに古いキャッシュが残っている可能性が高い。

TTLを下げるなら変更前に行う

計画的にDNSを切り替えるなら、事前にTTLを短くしておく方法がある。

レコードを書き換えた後でTTLを短くしても、すでに以前の長いTTLで保存されたキャッシュの寿命は縮まらない。以前のTTLが1時間で、あるResolverが変更直前に旧レコードを取得していれば、そのResolverでは最大で残り1時間近く旧レコードが使われる可能性がある。

TTLを使って切り替え時間を短くしたい場合は、旧TTLが失効する時間も見込んで、切り替えより前から準備しておく。

症状から最初に見る場所を絞る

すべての層を毎回調べる必要はない。症状によって、最初に見る場所はかなり絞れる。

症状最初に確認したい場所
強制再読み込みすると直るBrowser HTTP Cache
再読み込みしても古い静的ファイルが出るService Worker、CDN、HTML側のキャッシュ
APIリクエスト自体が発生していないClient Data Cache
APIレスポンス本文が古いCDN、Application Cache
DBは新しいがAPIが古いRedis、プロセス内キャッシュなど
リクエストごとに新旧が入れ替わる複数インスタンス、複数Edge、Cache Keyの違い
環境によって接続先IPが違うDNS Cache、TTL
権威DNSは新しいが一部Resolverだけ古いRecursive ResolverのDNS Cache

これは原因を断定するための表ではなく、調査の入口を選ぶための目安として使う。

最初から全部のキャッシュを消さない

開発中なら、ブラウザキャッシュ、CDN、Redisをすべて削除して直ることもある。ただ、その方法では原因になったキャッシュを特定できない。

本番では削除自体の影響も無視できない。CDNを全面PurgeすればOriginへのリクエストが一時的に増える可能性があり、Redisを広範囲に削除すればDB負荷が上がることもある。

HTTP経路なら、たとえば次の順で見ていく。

1. 変更元は新しいか
2. クライアントはリクエストを送っているか
3. CDNはHITしているか
4. Originは新しいレスポンスを返すか
5. Application Cacheは何を返しているか
6. DBは新しいか

DNSはこの経路とは分けて、権威DNS、Recursive Resolver、ローカルの名前解決結果を比較する。

対象を一つずつ確認していけば、原因を消してしまわずに古い値の出どころまでたどれる。

古い値が見えても、キャッシュとは限らない

「変更したのに古い値が見える」という症状は、キャッシュ以外でも起きる。

たとえば次のようなケースだ。

  • Rolling Deployment中で古いバージョンのインスタンスが残っている
  • 読み取りReplicaへの反映が遅れている
  • 複数リージョン間のデータ同期に時間差がある
  • リクエスト先の環境やHostを間違えている
  • フロントエンドが別のAPI endpointを参照している

キャッシュを無効化しても症状が変わらなければ、古い状態を返している別のインスタンスやReplicaがないか、接続先そのものが正しいかまで範囲を広げる。

ブラウザ、CDN、Redis、DNSでは仕組みが違うが、調査するときにやることはよく似ている。変更した場所からユーザーまでの経路を追い、実際にその値を返した場所を探す。