<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Stale-While-Revalidate on Tarragon</title><link>https://tarrragon.github.io/blog/tags/stale-while-revalidate/</link><description>Recent content in Stale-While-Revalidate on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 02 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/stale-while-revalidate/index.xml" rel="self" type="application/rss+xml"/><item><title>12.6 過期副本的延用：stale-while-revalidate 與 stale-if-error</title><link>https://tarrragon.github.io/blog/backend/12-http-caching/serving-stale/</link><pubDate>Fri, 02 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/12-http-caching/serving-stale/</guid><description>&lt;p>這篇整理快取可以不等驗證、直接給出已經過期的副本的條件。副本過期之後的一般處理（驗證）見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store&lt;/a>；本篇處理的是兩種例外：驗證可以在背景進行的時候，以及 origin 出錯的時候。新鮮度期限（副本不必詢問 origin 就能直接使用的時間）怎麼算，見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限&lt;/a>。&lt;/p>
&lt;h2 id="規範對過期副本的預設">規範對過期副本的預設&lt;/h2>
&lt;p>規範的預設是過期副本不能直接給出，只有兩種情形例外：快取連不上 origin，或 origin 與客戶端用指令明確允許（RFC 9111 §4.2.4）。&lt;code>no-cache&lt;/code>、&lt;code>must-revalidate&lt;/code>、&lt;code>proxy-revalidate&lt;/code> 與共用快取上的 &lt;code>s-maxage&lt;/code> 會連這兩種例外一起禁止，完整的規定見 &lt;a href="https://tarrragon.github.io/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store&lt;/a> 的〈過期副本在驗證前的處理〉。回應同時寫了 &lt;code>s-maxage&lt;/code> 與下面兩個指令時，兩個指令的情形不同。&lt;code>s-maxage&lt;/code> 在共用快取上禁止給出未經驗證的過期副本（RFC 9111 §4.2.4、§5.2.2.10）；§4.2.4 另一段列出的擴充指令許可，只在沒有這條禁止時才適用，所以 &lt;code>s-maxage&lt;/code> 與 &lt;code>stale-while-revalidate&lt;/code> 並用時，規範的答案是共用快取不延用過期副本。&lt;code>stale-if-error&lt;/code> 則不同：RFC 5861 §4 寫它可以「regardless of other freshness information」，與 RFC 9111 的禁止衝突，兩份規範之間沒有一致的答案。實作上，Varnish 7.7 的原始碼把 &lt;code>stale-while-revalidate&lt;/code> 的值直接當成寬限時間，不看 &lt;code>s-maxage&lt;/code>；CDN 怎麼處理這個組合，以各家文件與實測為準。&lt;/p>
&lt;p>明確允許的指令裡最常用的是 &lt;code>stale-while-revalidate&lt;/code> 與 &lt;code>stale-if-error&lt;/code>，兩者由 RFC 5861 定義。這份 RFC 的類別是 Informational，不在標準軌上（「This document is not an Internet Standards Track specification」），所以它的支援程度比 RFC 9111 的核心指令更依賴各實作。RFC 5861 寫到給出過期副本時要附 &lt;code>Warning&lt;/code> 標頭，而 &lt;code>Warning&lt;/code> 已在 RFC 9111 §5.5 廢止，實作多半不再送出它，不能拿它判斷回應是不是過期副本。&lt;/p>
&lt;h2 id="stale-while-revalidate">stale-while-revalidate&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Cache-Control: max-age=600, stale-while-revalidate=600&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這組標頭的意思是：新鮮度期限 600 秒；過期之後的 600 秒內，快取可以先把過期副本給出去，同時在背景向 origin 驗證或重新抓取，不讓請求等待（RFC 5861 §3）。過期之後允許給出過期副本的這段時間，本篇稱為 &lt;code>stale-while-revalidate&lt;/code> 的&lt;strong>窗口&lt;/strong>，長度就是指令後面的秒數。&lt;/p>
&lt;blockquote>
&lt;p>indicates that caches MAY serve the response in which it appears after it becomes stale, up to the indicated number of seconds.（RFC 5861 §3）&lt;/p>&lt;/blockquote>
&lt;p>&lt;code>stale-while-revalidate&lt;/code> 的使用條件：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>背景更新要靠請求觸發&lt;/strong>。只有在窗口內有請求進來，快取才會開始背景驗證；窗口內完全沒有請求，窗口結束後副本就照一般規則處理（RFC 5861 §3.1）。規範建議以請求觸發背景驗證，理由是避免放大攻擊（amplification attack，RFC 5861 §5）。&lt;/li>
&lt;li>&lt;strong>使用者可能拿到的最舊內容，年齡是 &lt;code>max-age&lt;/code> 加上 &lt;code>stale-while-revalidate&lt;/code> 的值&lt;/strong>。以 &lt;code>max-age=600, stale-while-revalidate=600&lt;/code> 為例，使用者最長可能拿到 1200 秒前產生的內容（RFC 5861 §3.1 的例子）。&lt;/li>
&lt;li>&lt;strong>窗口過後還沒更新成功，就不應該（SHOULD NOT）再給出過期副本&lt;/strong>，除非符合規範允許的另一種例外，例如連不上 origin（RFC 5861 §3）。&lt;/li>
&lt;/ul>
&lt;p>站內 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-while-revalidate/" data-link-title="Stale-While-Revalidate" data-link-desc="HTTP cache-control directive，cache 過期後仍立即回舊版、背景發出 origin request 拉取新版本更新快取">Stale-While-Revalidate&lt;/a> 卡從使用者體驗與新鮮度的取捨描述同一個機制。&lt;/p></description><content:encoded><![CDATA[<p>這篇整理快取可以不等驗證、直接給出已經過期的副本的條件。副本過期之後的一般處理（驗證）見 <a href="/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store</a>；本篇處理的是兩種例外：驗證可以在背景進行的時候，以及 origin 出錯的時候。新鮮度期限（副本不必詢問 origin 就能直接使用的時間）怎麼算，見 <a href="/blog/backend/12-http-caching/freshness-lifetime/" data-link-title="12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限" data-link-desc="快取在不詢問 origin 的情況下可以直接使用一份副本多久：新鮮度期限的取用順序、Age 標頭與副本年齡的計算、沒有明確期限時規範允許的啟發式期限，以及 Chromium、Firefox、Varnish、nginx 在啟發式期限上的差異">12.2 新鮮度期限的計算：max-age、s-maxage、Expires 與啟發式期限</a>。</p>
<h2 id="規範對過期副本的預設">規範對過期副本的預設</h2>
<p>規範的預設是過期副本不能直接給出，只有兩種情形例外：快取連不上 origin，或 origin 與客戶端用指令明確允許（RFC 9111 §4.2.4）。<code>no-cache</code>、<code>must-revalidate</code>、<code>proxy-revalidate</code> 與共用快取上的 <code>s-maxage</code> 會連這兩種例外一起禁止，完整的規定見 <a href="/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store</a> 的〈過期副本在驗證前的處理〉。回應同時寫了 <code>s-maxage</code> 與下面兩個指令時，兩個指令的情形不同。<code>s-maxage</code> 在共用快取上禁止給出未經驗證的過期副本（RFC 9111 §4.2.4、§5.2.2.10）；§4.2.4 另一段列出的擴充指令許可，只在沒有這條禁止時才適用，所以 <code>s-maxage</code> 與 <code>stale-while-revalidate</code> 並用時，規範的答案是共用快取不延用過期副本。<code>stale-if-error</code> 則不同：RFC 5861 §4 寫它可以「regardless of other freshness information」，與 RFC 9111 的禁止衝突，兩份規範之間沒有一致的答案。實作上，Varnish 7.7 的原始碼把 <code>stale-while-revalidate</code> 的值直接當成寬限時間，不看 <code>s-maxage</code>；CDN 怎麼處理這個組合，以各家文件與實測為準。</p>
<p>明確允許的指令裡最常用的是 <code>stale-while-revalidate</code> 與 <code>stale-if-error</code>，兩者由 RFC 5861 定義。這份 RFC 的類別是 Informational，不在標準軌上（「This document is not an Internet Standards Track specification」），所以它的支援程度比 RFC 9111 的核心指令更依賴各實作。RFC 5861 寫到給出過期副本時要附 <code>Warning</code> 標頭，而 <code>Warning</code> 已在 RFC 9111 §5.5 廢止，實作多半不再送出它，不能拿它判斷回應是不是過期副本。</p>
<h2 id="stale-while-revalidate">stale-while-revalidate</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Cache-Control: max-age=600, stale-while-revalidate=600</span></span></code></pre></div><p>這組標頭的意思是：新鮮度期限 600 秒；過期之後的 600 秒內，快取可以先把過期副本給出去，同時在背景向 origin 驗證或重新抓取，不讓請求等待（RFC 5861 §3）。過期之後允許給出過期副本的這段時間，本篇稱為 <code>stale-while-revalidate</code> 的<strong>窗口</strong>，長度就是指令後面的秒數。</p>
<blockquote>
<p>indicates that caches MAY serve the response in which it appears after it becomes stale, up to the indicated number of seconds.（RFC 5861 §3）</p></blockquote>
<p><code>stale-while-revalidate</code> 的使用條件：</p>
<ul>
<li><strong>背景更新要靠請求觸發</strong>。只有在窗口內有請求進來，快取才會開始背景驗證；窗口內完全沒有請求，窗口結束後副本就照一般規則處理（RFC 5861 §3.1）。規範建議以請求觸發背景驗證，理由是避免放大攻擊（amplification attack，RFC 5861 §5）。</li>
<li><strong>使用者可能拿到的最舊內容，年齡是 <code>max-age</code> 加上 <code>stale-while-revalidate</code> 的值</strong>。以 <code>max-age=600, stale-while-revalidate=600</code> 為例，使用者最長可能拿到 1200 秒前產生的內容（RFC 5861 §3.1 的例子）。</li>
<li><strong>窗口過後還沒更新成功，就不應該（SHOULD NOT）再給出過期副本</strong>，除非符合規範允許的另一種例外，例如連不上 origin（RFC 5861 §3）。</li>
</ul>
<p>站內 <a href="/blog/backend/knowledge-cards/stale-while-revalidate/" data-link-title="Stale-While-Revalidate" data-link-desc="HTTP cache-control directive，cache 過期後仍立即回舊版、背景發出 origin request 拉取新版本更新快取">Stale-While-Revalidate</a> 卡從使用者體驗與新鮮度的取捨描述同一個機制。</p>
<h2 id="stale-if-error">stale-if-error</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Cache-Control: max-age=600, stale-if-error=1200</span></span></code></pre></div><p>這組標頭的意思是：過期之後，如果向 origin 驗證時遇到錯誤，快取可以在 1200 秒內改給過期副本，不把錯誤轉給客戶端（RFC 5861 §4）；這 1200 秒是 <code>stale-if-error</code> 的窗口。規範對「錯誤」的定義是會產生 500、502、503、504 的情形，包括連不上 origin。</p>
<blockquote>
<p>when an error is encountered, a cached stale response MAY be used to satisfy the request, regardless of other freshness information.（RFC 5861 §4）</p></blockquote>
<p>它也可以出現在請求裡，作用範圍只限那一個請求。它的用途是把 origin 的短暫故障擋在快取這一層，站內 <a href="/blog/backend/knowledge-cards/stale-if-error/" data-link-title="Stale-If-Error" data-link-desc="HTTP cache-control directive、origin 出錯時用舊版頂著、確保使用者拿到有效回應">Stale-If-Error</a> 卡把它放在 origin 保護的脈絡下討論。</p>
<h2 id="stale-while-revalidate-與-stale-if-error-的實作支援差異">stale-while-revalidate 與 stale-if-error 的實作支援差異</h2>
<p>這兩個指令的實作支援差異很大。實測 Varnish 7.7 與 nginx 1.27（預設設定，origin 回 <code>max-age=5</code> 加上其中一個指令；最後一列不加 stale 指令，改用 <code>max-age=10</code>）：</p>
<table>
  <thead>
      <tr>
          <th>情形</th>
          <th>Varnish 7.7</th>
          <th>nginx 1.27 proxy_cache</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>stale-while-revalidate=30</code>，origin 正常</td>
          <td>過期後先給過期副本，同時在背景抓新的</td>
          <td>過期後同步回 origin 抓新的，請求要等；預設沒有打開 <code>proxy_cache_background_update</code></td>
      </tr>
      <tr>
          <td><code>stale-while-revalidate=30</code>，origin 停機</td>
          <td>窗口內給過期副本，超過後回 503</td>
          <td>窗口內給過期副本，超過後回 504</td>
      </tr>
      <tr>
          <td><code>stale-if-error=60</code>，origin 停機</td>
          <td>不認得這個指令，只靠自己的預設寬限時間延用過期副本，之後回 503</td>
          <td>窗口內給過期副本</td>
      </tr>
      <tr>
          <td>沒有任何 stale 指令，<code>max-age=10</code></td>
          <td>過期後仍有一段寬限時間（參數 <code>default_grace</code>）先給過期副本、背景更新</td>
          <td>過期後同步回 origin</td>
      </tr>
  </tbody>
</table>
<p>沒有任何 stale 指令時，Varnish 預設也會在過期後的一段時間內給出過期副本。規範的預設是不給出過期副本，所以這是比規範寬鬆的實作預設：沒寫任何 stale 指令的回應（例如商品價格）放在 Varnish 後面，過期後仍可能被給出一小段時間。</p>
<p>在瀏覽器上，WHATWG Fetch 規範的 <code>default</code> 快取模式（見 <a href="/blog/backend/12-http-caching/browser-cache-behavior/" data-link-title="12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁" data-link-desc="瀏覽器在不同操作下怎麼使用自己的 HTTP 快取：WHATWG Fetch 規範定義的快取模式、一般瀏覽、重新整理與強制重新整理各自送出的請求、請求端快取指令對中介快取的效力，以及與 HTTP 快取分開運作的 back/forward cache">12.7 瀏覽器端的快取行為：重新整理、強制重新整理與上一頁</a> 的〈Fetch 規範的快取模式〉）納入了 <code>stale-while-revalidate</code>：快取裡的副本已過期、但還在窗口內時，先回傳它，同時發出條件請求更新快取。<code>stale-if-error</code> 在瀏覽器的支援情形查各自的文件。CDN 對這兩個指令的支援與上限，同樣查各家文件。</p>
<h2 id="stale-while-revalidate-與-stale-if-error-的選擇">stale-while-revalidate 與 stale-if-error 的選擇</h2>
<p>兩個指令回答的是不同的問題：</p>
<ul>
<li><strong><code>stale-while-revalidate</code></strong> 回答「內容過期一小段時間，使用者拿到舊版可以接受嗎」。可以接受的內容（文章列表、商品介紹）用它換取每次都不必等 origin 回應；不能接受的內容（庫存數字、權限相關的回應）不用它。</li>
<li><strong><code>stale-if-error</code></strong> 回答「origin 故障時，給舊內容比給錯誤頁好嗎」。對多數讀取型內容答案是肯定的；對寫入結果、付款狀態這類「舊答案會造成錯誤行為」的內容，答案是否定的，那類回應用 <code>no-cache</code>，每次使用前都向 origin 確認；<code>must-revalidate</code> 只適合新鮮期間內可以接受舊值、過期後不能延用的內容（兩者的差別見 <a href="/blog/backend/12-http-caching/revalidation/" data-link-title="12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store" data-link-desc="副本過期之後快取怎麼向 origin 確認它是否仍然有效：ETag 與 Last-Modified 兩種驗證子、條件請求與 304 回應、no-cache、no-store 與 must-revalidate 的語意差別，以及共用快取實作在驗證上的預設行為">12.3 過期副本的驗證：條件請求、304 回應、no-cache 與 no-store</a>）。<code>must-revalidate</code> 禁止給出過期副本是規範層的要求；Varnish 7.7 的原始碼裡，寬限時間預設取 <code>default_grace</code>，不因 <code>must-revalidate</code> 關閉，所以預設設定下帶 <code>must-revalidate</code> 的副本仍會在寬限時間內被給出，要在 VCL 裡另外處理（<code>no-cache</code> 的回應 Varnish 預設不保存，沒有這個問題）。</li>
</ul>
<p>兩個窗口都會延長內容改版後全面生效的時間：正常運作時延長的是 <code>stale-while-revalidate</code> 的窗口，origin 出錯期間延長的是 <code>stale-if-error</code> 的窗口，加在 <a href="/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期</a> 的〈等待到期與改版全面生效的時間〉列出的其他成分之上。決定窗口長度時，要同時看 <a href="/blog/backend/12-http-caching/updating-stored-responses/" data-link-title="12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期" data-link-desc="origin 想改變已經被快取保存的副本時可用的手段：規範定義的失效、CDN 與反向代理提供的 purge、換網址的版本化做法與 immutable、只作用於瀏覽器的 Clear-Site-Data，以及每一種手段能傳到哪幾種快取">12.5 已保存副本的更新方式：失效、purge、版本化網址與等待到期</a> 裡各種更新手段的作用範圍：瀏覽器、CDN、反向代理這幾層快取當中，哪一層的副本清得掉、哪一層只能等到期。</p>
]]></content:encoded></item></channel></rss>