Cloudflare Cache と stale-while-revalidate

## Update
2025/04

私のフリープランのゾーンに Async SWR が来た模様です。

![Screenshot 2025-04-04 at 15.11.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/fec3d27d-5881-4413-91cd-454ddc726a17.png)

2024/04のCloudflare[ブログ](https://blog.cloudflare.com/ja-jp/browser-rendering-api-ga-rolling-out-cloudflare-snippets-swr-and-bringing-workers-for-platforms-to-our-paygo-plans/#jin-ri-gong-kai-stale-while-revalidateniyorufei-tong-qi-zai-baridesiyon)

## やること

Cloudflare の Cache がオリジンからの stale-while-revalidate にどう反応するか、を確認。

### stale-while-revalidate 拡張

<details><summary> <a href="https://datatracker.ietf.org/doc/html/rfc5861"> [RFC 5861] The stale-while-revalidate Cache-Control Extension</a> </summary>

```
   When present in an HTTP response, the stale-while-revalidate Cache-
   Control extension indicates that caches MAY serve the response in
   which it appears after it becomes stale, up to the indicated number
   of seconds.
     stale-while-revalidate = "stale-while-revalidate" "=" delta-seconds

   If a cached response is served stale due to the presence of this
   extension, the cache SHOULD attempt to revalidate it while still
   serving stale responses (i.e., without blocking).

   Note that "stale" implies that the response will have a non-zero Age
   header and a warning header, as per HTTP's requirements.

   If delta-seconds passes without the cached entity being revalidated,
   it SHOULD NOT continue to be served stale, absent other information
```
</details>

## テスト

- Stale 状態 (かつ stale-while-revalidate の期間内) のオブジェクトに対してリクエスト
- Eyeball が受け取るレスポンスが Stale か Fresh かを確認
  - 以下のシーケンス図について 
    - 茶色は Eyeball に Stale が返ってくる部分
    - 緑色は Eyeball に Fresh が返ってくる部分（再検証後）


### 結果

Cloudflare は下記の B の動きをしている。
（A への対応は [Cache Reserve](https://developers.cloudflare.com/cache/advanced-configuration/cache-reserve/) でも使われている [Pingora](https://blog.cloudflare.com/how-we-built-pingora-the-proxy-that-connects-cloudflare-to-the-internet/) 投入待ちか。）



#### A. Asynchronous

- ノンブロック
- 全部 Stale から返しとく

``` mermaid
sequenceDiagram
    rect rgb(232, 199, 147)
    activate Cache
    participant Eyeball A
    participant Eyeball B
    Eyeball A->> Cache: 1st
    Cache ->> Eyeball A: Stale 
    Cache -->>Origin: Revalidate
    Eyeball B->> Cache: 2nd
    Cache ->> Eyeball B: Stale 
    deactivate Cache
    end
    rect rgb(143, 206,0)
    Origin -->> Cache: Fresh
    activate Cache
    deactivate Cache
    end
```

#### B. 1つ目だけ Synchronous、後続は Asynchronous

- 1つ目は再検証でブロックされる（再検証を待つ）
- 後続は Stale 返しとく

``` mermaid
sequenceDiagram
    rect rgb(232, 199, 147)
    participant Eyeball A
    participant Eyeball B
    activate Cache
    Eyeball A->> Cache: 1st
    Cache -->>Origin: Revalidate
    Eyeball B->> Cache: 2nd
    Cache ->> Eyeball B: Stale 
    deactivate Cache
    end
    rect rgb(143, 206,0)
    Origin -->> Cache: Fresh
    activate Cache
    Cache ->> Eyeball A: Fresh 
    deactivate Cache
    end
```

## HTTP ヘッダー

### オリジンの cache-control ヘッダー

`cache-control:` max-age=60, stale-while-revalidate=30
`etag`: あり
`last-modified`: あり

cache-control ヘッダーについて → [Mdn](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)

### Cloudflare Cache の返す cf-cache-status ヘッダー
Eyeball が受け取ったレスポンスが Stale か Fresh かを判断する一つとして cf-cache-status を確認する。

cf-cache-status ヘッダーについて → [Cloudfalre](https://developers.cloudflare.com/cache/concepts/default-cache-behavior/#cloudflare-cache-responses)

### オリジンのオブジェクトに変更がないとき

ETag 同じ。 

- Eyeball A（1つ目）が受ける cf-cache-status は `REVALIDATED`
- Eyeball B（2つ目）が受ける cf-cache-status は `UPDATING`

``` mermaid
sequenceDiagram
    rect rgb(232, 199, 147)
    participant Eyeball A
    participant Eyeball B
    activate Cache
    Eyeball A->> Cache: GET
    Cache -->>Origin: GET (if-none-match, if-modified-since)
    Eyeball B->> Cache: GET
    Cache ->> Eyeball B: cf-cache-status: UPDATING, etag: "SAME", age:60+n 
    deactivate Cache
    end
    rect rgb(143, 206,0)
    Origin -->> Cache: 304 Not Modified
    activate Cache
    Cache ->> Eyeball A: cf-cache-status: REVALIDATED, etag: "SAME" 
    deactivate Cache
    end
```

### オリジンのオブジェクトに変更があるとき

ETag 変わる。

- Eyeball A（1つ目）が受ける cf-cache-status は `EXPIRED`
- Eyeball B（2つ目）が受ける cf-cache-status は `UPDATING`

``` mermaid
sequenceDiagram
    rect rgb(232, 199, 147)
    participant Eyeball A
    participant Eyeball B
    activate Cache
    Eyeball A->> Cache: GET
    Cache -->>Origin: GET (if-none-match, if-modified-since)
    Eyeball B->> Cache: GET
    Cache ->> Eyeball B: cf-cache-status: UPDATING, etag: "SAME", age:60+n 
    deactivate Cache
    end
    rect rgb(143, 206,0)
    Origin -->> Cache: 200 OK
    activate Cache
    Cache ->> Eyeball A: cf-cache-status: EXPIRED, etag: "NEW"
    deactivate Cache
    end
```

## stale-while-revalidate を無視する設定

[Cache Rules](https://developers.cloudflare.com/cache/how-to/cache-rules/) を使って、stale-while-revalidate を無視する設定にしたときの動作。

<details><summary>Serve Stale Content</summary>

```
Enable or disable serving stale content while updating from the origin server. If serving stale content is disabled, origin cache-control headers will be used to tell Cloudflare how to handle content from the origin.
```
</details>

![Screenshot 2023-07-03 at 21.25.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2218045/37c9683b-28d3-4292-8ad9-99f09a3a6c3d.png)

### オリジンのオブジェクトに変更がないとき

``` mermaid
sequenceDiagram
    participant Eyeball A
    participant Eyeball B
    rect rgb(232, 199, 147)
    activate Cache
    Eyeball A->> Cache: GET
    Cache -->>Origin: GET (if-none-match, if-modified-since)
    Eyeball B->> Cache: GET
    Cache -->>Origin: GET (if-none-match, if-modified-since)
    end
    deactivate Cache
    rect rgb(143, 206,0)
    Origin -->> Cache: 304 Not Modified
    activate Cache
    Origin -->> Cache: 304 Not Modified
    Cache ->> Eyeball A: cf-cache-status: REVALIDATED, etag: "SAME" 
    Cache ->> Eyeball B: cf-cache-status: REVALIDATED, etag: "SAME" 
    end
    deactivate Cache
```

### オリジンのオブジェクトに変更があるとき

``` mermaid
sequenceDiagram
    participant Eyeball A
    participant Eyeball B
    rect rgb(232, 199, 147)
    activate Cache
    Eyeball A->> Cache: GET
    Cache -->>Origin: GET (if-none-match, if-modified-since)
    Eyeball B->> Cache: GET
    Cache -->>Origin: GET (if-none-match, if-modified-since)
    end
    deactivate Cache
    rect rgb(143, 206,0)
    Origin -->> Cache: 200 OK
    activate Cache
    Origin -->> Cache: 200 OK
    Cache ->> Eyeball A: cf-cache-status: EXPIRED, etag: "NEW"
    Cache ->> Eyeball B: cf-cache-status: EXPIRED, etag: "NEW"
    end
    deactivate Cache
```

### テスト環境メモ
- Stale 状態のコンテンツを吐くため、オリジンに遅延を入れた（`tc`） 。
遅延なしだとオリジンからのレスポンスが早く届き、2発目へのレスポンスは Fresh  で `cf-cache-status: HIT` だった。
- リクエストパスに対する [Origin Cache-control](https://developers.cloudflare.com/cache/concepts/cache-control/)  設定は無効
有効にしても結果変わらず（エンタープライズプランではデフォルト[無効](https://developers.cloudflare.com/support/page-rules/understanding-and-configuring-cloudflare-page-rules-page-rules-tutorial/)）
- [Edge Cache TTL](https://developers.cloudflare.com/cache/how-to/edge-browser-cache-ttl/#edge-cache-ttl) は設定しない


## Link
[Cloudflare Community: when-will-cloudflare-fully-support-stale-while-revalidate-with-asynchronous-background-revalidation](https://community.cloudflare.com/t/when-will-cloudflare-fully-support-stale-while-revalidate-with-asynchronous-background-revalidation/287764)
[Google](https://web.dev/ads-case-study-stale-while-revalidate/)
[Fastly](https://developer.fastly.com/learning/concepts/stale/)

## その他
オリジンへのリクエスト抑制の対策として、[Tiered Cache](https://developers.cloudflare.com/cache/how-to/tiered-cache/) の early eviction でおこぼれした場合の備えは  [Cache Reserve](https://blog.cloudflare.com/introducing-cache-reserve/) が使える。