Cloudflare R2のカスタムドメインで上書きしてもファイルが更新されない
Cloudflare R2 のカスタムドメインでファイルを上書きアップロードしたのに、ブラウザでは古いファイルが返ってくる——。R2 オリジンは新しいのに、だ。結論から言うと、cache-control: immutable を付けた配信は CDN(とブラウザ)が最大1年キャッシュを握り続けるため、同名上書きでは更新が反映されない。原因の切り分けとパージ手順、そして「そもそも踏まない設計」までまとめる。
何が起きているか
R2 のカスタムドメイン(例 dl.example.com)は Cloudflare CDN を経由する。レスポンスに
cache-control: public, max-age=31536000, immutable
を付けると、エッジに最大1年キャッシュされる。この状態で同名ファイルを上書きアップロードしても、CDN は HIT のまま古い実体を配り続ける。R2 オリジンだけが新しくなり、ユーザーには届かない。
まず切り分ける: オリジンは更新されているか
CDN を経由せず、R2 オリジンの実体を直接取って確認する。
wrangler r2 object get <bucket>/<key> --file /tmp/check --remote
md5 /tmp/check # アップロードしたファイルと一致するか
オリジンが新しければ、問題は CDN キャッシュだと確定できる。
パージする
Cloudflare ダッシュボードの Caching → Configuration → Purge Cache → Custom Purge → By URL で対象URLを指定する。API なら zone:cache_purge 権限のトークンで:
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data '{"files":["https://dl.example.com/app.zip"]}'
検証は次のとおり。cf-cache-status が MISS になり、etag が入れ替わっていれば成功だ。
curl -sI https://dl.example.com/app.zip | grep -iE 'cf-cache-status|etag'
盲点: ブラウザキャッシュは CDN パージでは消えない
ここが一番ハマるところ。immutable は CDN だけでなくブラウザにも効く。そのため、一度ダウンロード済みのブラウザは、CDN をパージしてもローカルキャッシュから古い版を出し続ける。
curlでは正しい版が取れるのに、ブラウザのダウンロードだけ壊れた版が来る——という食い違いで顕在化する。curl検証だけでは見抜けない。
応急処置として、URL にクエリを足す(app.zip?v=2)とキャッシュキーが変わってバイパスできる。
そもそも踏まない設計
王道は、バージョン付きファイル名にして同名上書きを避けることだ。
app-1.2.0.zip # 中身は二度と変えない
app-1.2.1.zip # 変更は新ファイルで
immutable を使うなら「中身は二度と変えない」が大前提。バージョン据え置きで差し替えると、CDN にもブラウザにもこの沼が残る。
まとめ
immutableキャッシュは同名上書きでは更新されない(CDN が古い実体を保持)- 切り分けは
wrangler r2 object get --remoteでオリジンを直接確認 - パージは By URL か
zone:cache_purgeAPI。cf-cache-status: MISSで確認 - ブラウザキャッシュは CDN パージで消えない。クエリ付与で回避
- 本来はバージョン付きファイル名で同名上書きを避けるのが正解