ブログ一覧

Cloudflare の Purge Everything で Workers Cache が消えない理由

更新: Web開発者向け

Cloudflare 上で動かしている Astro 製の CMS「EmDash」 のサイトで、ヘッダーの表示を変えたいと思いました。エッジキャッシュに古い HTML が載っているはずなので、ダッシュボードの Caching → Configuration → Purge Everything を押しましたが、キャッシュは消えませんでした。

押す前と押した後で、age が増えているだけだった

cf-cache-status と age を見れば、キャッシュが消えたかどうかは分かります。消えていれば次のアクセスは MISS になり、age はリセットされます。

押す前。

$ curl -s -D- -o /dev/null https://example.com/ | grep -iE 'cf-cache-status|^age:'
cf-cache-status: HIT
age: 6

Purge Everything を実行し、数秒待ってから同じコマンドを叩きました。

cf-cache-status: HIT
age: 146

さらに 30 秒ほど置いて、もう一度。

cf-cache-status: HIT
age: 180

age が 6 → 146 → 180 と増え続けています。これは「146 秒前にキャッシュされたものをそのまま返している」という意味で、purge されていれば起こりません。別のパスでも同じでした。ダッシュボードの操作自体は成功していて、エラーも出ていません。

Workers Cache にはゾーンの purge が届かない

原因は、消そうとしていたキャッシュが「ゾーンのキャッシュ」ではなかったことでした。

Astro の Cloudflare アダプタでルートキャッシュを有効にすると(HTML をエッジにキャッシュさせる設定です。Cloudflare で HTML がキャッシュされないのは既定の仕様)、ビルド時に wrangler の設定へ cache が注入されます。

$ node -e "console.log(JSON.stringify(require('./dist/server/wrangler.json').cache))"
{"enabled":true}

これは Workers Caching という機能で、Worker の前段に置かれる専用のキャッシュになります。ゾーンの CDN キャッシュとは別の層で、Cloudflare のドキュメントにも次のように書かれています。

A Worker cannot reach into another Worker's cache, an entrypoint cannot reach into another entrypoint's cache, and no zone-level purge (via the dashboard, API, or Terraform) affects Workers Caching content.

ダッシュボードからでも、API からでも、Terraform からでも、ゾーンレベルの purge は Workers Caching の中身に影響しないと明記されています。実測で age が増え続けたのは、仕様どおりの挙動でした。

トークンに Cache Purge の権限を足せば API から消せるのではとも考えましたが、purge の宛先が違うので、権限を足しても消せません。

消せるのは Worker の中からだけ

Workers Caching を失効させる手段は、Worker のコードの中から呼ぶものしかありません。

import { cache } from "cloudflare:workers";

await cache.purge({ tags: ["posts"] });        // タグ指定
await cache.purge({ purgeEverything: true });  // 全部

ctx.cache.purge() でも同じことができます。CMS がコンテンツの保存や公開でキャッシュを自動失効させている場合、内部で呼んでいるのもこの purge() です。

つまり CI やローカルの端末など Worker の外からは、Workers Caching を消せません。デプロイの後処理でキャッシュを消す手順を組もうとして、この制約で先に進めなくなりました。

デプロイ時の purge はそもそも要らなかった

調べ続けるうちに、デプロイ後に purge が要るという前提が間違っていたと分かりました。

Workers Caching は Worker のバージョンをキャッシュキーに含んでいます。設定の cross_version_cache が既定の false のとき、こうなります。

the Worker version is part of the cache key. Each deployed version has its own isolated cache, so a new deployment starts from an empty cache and never serves responses written by a previous version.

新しくデプロイしたバージョンは空のキャッシュから始まり、前のバージョンが書いたレスポンスを配信することは決してないということです。ドキュメントには「version-specific purging is unnecessary」とも書かれています。

デプロイ直後に確かめると、そのとおりでした。8 サイトすべてで 1 回目が MISS、2 回目以降が HIT に変わります。

1 回目
example-a.com    MISS
example-b.com    MISS
example-c.com    MISS

2 回目
example-a.com    cf-cache-status: HIT age: 16
example-b.com    cf-cache-status: HIT age: 14
example-c.com    cf-cache-status: HIT age: 11

コードを変更してデプロイすれば、古い HTML は配信されません。デプロイ後に purge を実行する仕組みは作る必要が無く、書きかけていたスクリプトと CI のステップは捨てました。

直しても残るもの

これで「デプロイしたのに反映されない」は起きないと分かりましたが、コンテンツ以外の変更が反映されない場合は残っていました。

CMS がキャッシュを自動失効させる範囲は、コンテンツの保存・公開・削除に限られていることが多いようです。サイト設定やメニュー、記事へのタグ付けを変えても、キャッシュを失効させないことがあります。これらの変更はデプロイを伴わないので、バージョンごとにキャッシュが分かれる仕組みでも失効しません。エッジキャッシュの保持時間を長く取っているほど、古い表示が長く残ります。

この問題の詳細と、CMS 本体を変更せずに直す方法は EmDash で記事にタグを付けても本番に反映されない原因 に分けました。

ゾーンの purge では消えず、デプロイで空になる

  • ゾーンの Purge Everything では Workers Caching は消えない。ダッシュボードでも API でも Terraform でも同じ
  • 消えたかどうかは cf-cache-status と age で判定する。age が単調増加していれば消えていない
  • Workers Caching を消せるのは Worker のコードの中からだけ。cache.purge({ tags }) または cache.purge({ purgeEverything: true })
  • デプロイすればキャッシュは空から始まる(cross_version_cache が既定の false のとき)。デプロイ後の purge 処理は不要
  • 確かめるときは HEAD(curl -sI)を使わない。キャッシュから返るので Worker に届かず、age を進めるだけになる。curl -s -D- -o /dev/null のように本文も取る GET で見る(Cloudflare で purge しても age が伸び続けるのは壊れていない)

キャッシュが消えないとき、まず権限と反映の遅延を疑いましたが、実際には消す対象を取り違えていました。Workers Caching はゾーンのキャッシュと名前も見た目も近いのに、消す手段は Worker のコードの中からの purge だけです。

この記事をシェア