入门指南
源站 502 时缓存还能兜住吗:两条 stale 指令都是 MAY,窗口里没请求就不会去验证
stale-while-revalidate 与 stale-if-error 都只给缓存一个「可以」,不是承诺:一个允许先回旧的再后台验证,一个允许在 500/502/503/504 时顶上。窗口从变陈旧那一刻起算,没有请求就不会有验证,遇到 must-revalidate 直接失效。
两条指令都只允许缓存「先给旧的」,不给任何更新承诺。

先看答案
- 适合谁
- 自己发静态站、开了 CDN、以为「源站偶尔 502 缓存能兜住」的人
- 把 stale-while-revalidate 当成「自动更新」开关的人
- 给接口配了 must-revalidate,又指望 stale-if-error 救场的人
- 你会获得
- 先算清窗口有多长,再确认有没有人替你触发验证。
- 发布时间
- 实质更新
- 发布
- AOPC 编辑部
60 秒结论
一分钟看懂这篇文章
两条指令都只允许缓存「先给旧的」,不给任何更新承诺。
人负责
- 判断源站抖动时旧内容能顶多久
- 分清窗口起点与总时长
AI 负责
- 读 Cache-Control 里的两个 stale 指令
- 核对 Age 与验证是否真的发生
- 确认窗口长度
- 核对闸门指令
- 看有无流量触发
- 按层定位旧副本
直接答案
「源站偶尔抽风,我加了 stale-if-error,缓存应该能兜住吧?」
能不能兜住,取决于三件事,而且没有一件是这条指令自己说了算:窗口有多长、旧的有没有被存下来、以及中间有没有一条 must-revalidate 把它按住。
两条指令各管一个方向:
| 指令 | 允许缓存做什么 | 谁在什么条件下会被救 |
|---|---|---|
stale-while-revalidate=N |
变陈旧后的 N 秒内,先回旧的,同时后台去验证 | 正常访问的人,省掉一次阻塞等待 |
stale-if-error=N |
变陈旧后的 N 秒内,验证拿到错误时回旧的 | 源站正在 500/502/503/504 时的人 |
三句话记住:
- 两条都是
MAY——缓存可以不做,做了也不算违约; - 窗口从「变陈旧」那一刻起算,所以总时长是
max-age + N,不是N; must-revalidate、no-cache、s-maxage、proxy-revalidate在场时,两条都可能直接失效(RFC 9111 的硬闸门,见第五节)。
下面按「它允许什么 → 窗口怎么算 → 什么会关掉它 → 怎么核对」拆开。
一、它们改的是「过期之后」,不是「寿命」
先复习一个容易混的点:max-age 决定的是新鲜期,也就是「不联系源站就能直接用」的那段时间。过了这段时间,响应就是陈旧(stale)的:
A “fresh” response is one whose age has not yet exceeded its freshness lifetime. Conversely, a “stale” response is one where it has.
两条 stale 指令都不动这个界限。RFC 5861 在 stale-if-error 那一节特意写了一句:
Note that this directive does not affect freshness
stale-while-revalidate 同理——它没有自己的「寿命」参数,只是允许在陈旧之后再往外多服务一段时间。
所以正确的心智模型是一根时间轴,而不是一个开关。取 max-age=600 分别加两条 600 秒的扩展:
0 ─────────── 600 ─────────── 1200 ─────────── 1800 ──────▶ Age(秒)
新鲜:直接回 swr 窗口:先回旧的+后台验证 (sie 窗口内出错可回旧的)
sie 窗口:出错时回旧的
这就是 RFC 5861 自己给的算法:
Generally, servers will want to set the combination of max-age and stale-while-revalidate to the longest total potential freshness lifetime that they can tolerate. … For example, with both set to 600, the server must be able to tolerate the response being served from cache for up to 20 minutes.
「must be able to tolerate」是这句的重点:它不是「缓存最多只留 20 分钟」,而是「你要能接受最多 20 分钟的旧内容」。窗口是给读者的耐心设的,不是给缓存的容量设的。
二、stale-while-revalidate:先把旧的给你,再去问
RFC 5861 对它的定义只有一句:
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.
紧接着是「后台验证」这个动作,注意它的语气:
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).
SHOULD attempt——应当尝试,不是保证成功,也不是必须尝试。它想省掉的是什么,引言说得很清楚:
The stale-while-revalidate HTTP Cache-Control extension allows a cache to immediately return a stale response while it revalidates it in the background, thereby hiding latency (both in the network and on the server) from clients.
两个延迟一起藏:网络往返,和源站自己变慢的那一段。这也解释了它最适合的场景——内容变化不频繁、但源站偶尔慢的页面。如果你的内容是「一分钟内必须准」,这条指令帮不了你,它只会让你更快地看到旧东西。
还有一层很容易被忽略:先回旧的不等于这次请求之后内容就更新了。后台验证如果成功,新的表示是给后来的请求的;发出去的那份旧响应不会追回来(编辑推论,依据是响应的单向性)。别把「用户这次没等」当成「用户这次看到的是新的」。
三、窗口里没有请求,就没有任何验证
这是两条指令最常被误读的地方,而且 RFC 5861 是明写的:
Since asynchronous validation will only happen if a request occurs after the response has become stale, but before the end of the stale-while-revalidate window, the size of that window and the likelihood of a request during it determines how likely it is that all requests will be served without delay.
验证是挂在进来的请求上的,不是定时任务。没有请求,就没有验证;窗口过了,连这个机会也没了:
If validation is inconclusive, or if there is not traffic that triggers it, after 30 seconds the stale-while-revalidate function will cease to operate, and the cached response will be “truly” stale (i.e., the next request will block and be handled normally).
「下一个请求会阻塞」——这句话说明窗口结束后并没有变成「永久回旧的」,而是退回到最普通的缓存行为:等验证。
流量稀疏的站点因此有个反直觉的结论:
If the window is too small, or traffic is too sparse, some requests will fall outside of it, and block until the server can validate the cached response.
你以为是「第一个访客承担延迟、后面的人都快」,实际更常见的是「每个访客都恰好是第一个」。
还有一个安全侧的注记,说明这个「挂在请求上」是设计而不是省事:
It is suggested that such validation be predicated upon an incoming request, to avoid the possibility of an amplification attack (as can be seen in some other pre-fetching and automatic refresh mechanisms).
如果缓存自己定时去刷,一个没人访问的页面也能持续把请求打到源站——放大攻击的现成模板。所以「窗口内一定会有人替我验证」这个假设,规范作者从一开始就没打算让你有。
四、stale-if-error:这里的「错误」是四个状态码
定义同样简短,但多了半句限定:
The stale-if-error Cache-Control extension indicates that when an error is encountered, a cached stale response MAY be used to satisfy the request, regardless of other freshness information.
regardless of other freshness information 读起来很像「什么都能顶」,但「错误」这个词在同一节被收窄了:
In this context, an error is any situation that would result in a 500, 502, 503, or 504 HTTP response status code being returned.
四个码,就这四个。 403、404、429、超时到没有响应……都不在这个定义里。这一点和它的引言举例有张力,值得单独指出来:
The stale-if-error HTTP Cache-Control extension allows a cache to return a stale response when an error – e.g., a 500 Internal Server Error, a network segment, or DNS failure – is encountered, rather than returning a “hard” error.
引言把 a network segment(网络中断)与 DNS failure 也举成了例子,而第四节的定义只列状态码。遇到这种规范内部的宽窄不一,工程上应当按更窄的规范定义理解,把引言的列举当成背景(编辑判断)。也就是说:别指望它在「连不上源站」时一定顶上——那种情况下缓存到底算不算「遇到会导致 504 的情形」,取决于实现怎么归类,一手材料没有规定。
窗口的算法与 swr 不同,它算的是「陈旧了多久」:
Its value indicates the upper limit to staleness
when the cached response is more stale than the indicated amount, the cached response SHOULD NOT be used to satisfy the request, absent other information
RFC 5861 给了一个带数字的例子,正好可以当验收用例:max-age=600, stale-if-error=1200,第 900 秒去验证、拿到 500,则回 200 且 Age: 900;超过 1800 秒之后:
After the age is greater than 1800 seconds (i.e., it has been stale for 1200 seconds), the cache must write the error message through.
「write the error message through」——错误要原样透出去。这时候别怀疑配置写错了,是窗口真的用完了。
一个容易忽略的作用域差异:
When used as a request Cache-Control extension, its scope of application is the request it appears in; when used as a response Cache-Control extension, its scope is any request applicable to the cached response in which it occurs.
它既能写在请求里(只影响这一次),也能写在响应里(影响这条响应的所有后续请求)。所以线上抓包看到客户端请求头里带着 stale-if-error,不代表源站配了;反过来,源站配了也不会出现在请求头里。
五、三道闸门:什么时候两条都失效
RFC 5861 是 2010 年的文档,它定义的扩展现在被 RFC 9111 统一收在一节里管。这一节是全文最该背下来的地方:
A cache MUST NOT generate a stale response if it is prohibited by an explicit in-protocol directive (e.g., by a no-cache response directive, a must-revalidate response directive, or an applicable s-maxage or proxy-revalidate response directive; see Section 5.2.2).
A cache MUST NOT generate a stale response unless it is disconnected or doing so is explicitly permitted by the client or origin server (e.g., by the max-stale request directive in Section 5.2.1, extension directives such as those defined in [RFC5861], or configuration in accordance with an out-of-band contract).
两条 MUST NOT,方向相反:
- 第一道:只要响应里有
no-cache、must-revalidate、适用的s-maxage或proxy-revalidate,就不许发陈旧的。这是内容方的否决权。 - 第二道:除此之外,还必须有人明确允许——客户端用
max-stale、源站用 RFC 5861 这类扩展、或者一份带外约定。没有允许,也不许发。
所以 stale-if-error 不是「额外开一个口子」,而是把自己塞进第二道闸门里的那个「explicitly permitted」。第一道闸门在它前面,先判。
这正是 no-cache 那一节的原话:
This allows an origin server to prevent a cache from using the response to satisfy a request without contacting it, even by caches that have been configured to send stale responses.
「even by caches that have been configured to send stale responses」——配置了也没用。must-revalidate 更硬:
In all circumstances, a cache MUST NOT ignore the must-revalidate directive; in particular, if a cache is disconnected, the cache MUST generate an error response rather than reuse the stale response.
「In all circumstances」与「MUST NOT ignore」,再加上「断网时宁可报错也不回旧的」。这直接推翻了很多人对 stale-if-error 的期待:给一个带 must-revalidate 的页面配 stale-if-error,等于没配(编辑推论,依据是上面的优先级)。
顺便说一个反直觉的事实:第二道闸门里,「断网」本身就是被允许的情形。
A cache MUST NOT generate a stale response unless it is disconnected or …
也就是说,一个与源站失联的缓存本来就可以回旧的,不需要你写任何指令。stale-if-error 真正扩展的,是源站还在、但回了 5xx 这种「连着却不好」的情形;而 must-revalidate 恰恰把「断网也能顶」这条路堵死,要求它宁可 504。
六、和 must-revalidate 的正面冲突
把两条并排放在一个响应里,会发生什么?规范给的裁决原则是:
If directives conflict (e.g., both max-age and no-cache are present), the most restrictive directive should be honored.
「最严格的那个赢」。所以下面这几行配置,效果并不一样:
| 响应头 | 断网时 | 源站 5xx 时 | 陈旧之后正常请求 |
|---|---|---|---|
max-age=600, stale-if-error=1200 |
可回旧的(第二道闸门的 disconnected) |
窗口内可回旧的 | 阻塞验证 |
max-age=600, stale-if-error=1200, must-revalidate |
必须报错 | 必须报错(第一道闸门) | 阻塞验证 |
max-age=600, stale-if-error=1200, no-cache |
必须验证 | 必须先验证 | 每次都要验证 |
max-age=600, stale-while-revalidate=600, must-revalidate |
报错 | 报错 | 窗口内先回旧的再验证?——见下 |
最后一行故意留了个问号:must-revalidate 说的是「变陈旧后未成功验证前 MUST NOT reuse」,而 swr 说的是「可以先用旧的、同时验证」。两句在字面上是冲突的,按「最严格优先」应当按 must-revalidate 处理(编辑判断)。这种组合不要写——不是因为它一定坏,而是因为不同实现的取舍可能不同,而你无法从响应头看出它选了哪个。
must-revalidate 该用在哪,规范也给了判据:
The must-revalidate directive ought to be used by servers if and only if failure to validate a request could cause incorrect operation, such as a silently unexecuted financial transaction.
「if and only if」——只在「用旧的会导致操作错误」时才用。静态博客的首页显然不属于这一类;一个显示余额的接口属于。把这条判据和 stale-if-error 放在一起看,就能理解为什么两者天生互斥:一个说「旧一点没关系」,另一个说「旧一点都不行」。
七、Warning 头:2010 年写的那句,2022 年已经作废
RFC 5861 有两处都提到「陈旧的响应会带 Warning 头」:
Note that “stale” implies that the response will have a non-zero Age header and a warning header, as per HTTP’s requirements.
而 RFC 9111 已经把 Warning 整个废掉了:
The “Warning” header field was used to carry additional information about the status or transformation of a message that might not be reflected in the status code. This specification obsoletes it, as it is not widely generated or surfaced to users.
IANA 在 Warn Code 注册表上也加了同样的注记,RFC 9111 §8.3 原样引了那句话:The Warning header field (and the warn codes that it uses) has been obsoleted for HTTP per [RFC9111].
所以「陈旧响应一定带 Warning 头」在今天是不成立的:规范不再要求,实测也常常没有。还能核对的只剩 Age:
The Age field value is the cache’s estimate of the number of seconds since the origin server generated or validated the response.
The presence of an Age header field implies that the response was not generated or validated by the origin server for this request. However, lack of an Age header field does not imply the origin was contacted.
后一句很关键:没有 Age 不等于「这次是源站现生成的」——它只是「没有证据」。所以「看有没有 Age 判断是不是命中缓存」只能单向用:有 Age 一定不是本次源站产出,没有则什么都不能说明。
八、两层缓存:窗口不会因为多一层而变长
一人公司的典型链路是「浏览器 → CDN → 源站」,两层都是缓存,两层都会算自己的新鲜期。直觉上会以为「两层各 600 秒,那不是能顶 20 分钟?」,但 Age 是累加的:
The Age value is therefore the sum of the time that the response has been resident in each of the caches along the path from the origin server, plus the time it has been in transit along network paths.
下游缓存拿到的 Age 已经包含上游停留的时间,所以窗口是共享的一份预算,不是每层各发一份(编辑推论,依据是上面这句对 Age 的定义)。这也是为什么「CDN 上看着还很新、浏览器里已经旧了」这种错觉会出现:两边看到的是同一个 Age 的不同切片。
由此得到一个实际结论:你无法控制哪一层替你触发后台验证。CDN 的流量大,它更可能触发;单人博客的浏览器端几乎不会。所以「配了 swr,源站压力就小了」这个期待,取决于你的流量落在哪一层。
还有一层更容易被忽略——部署不会自动让缓存失效。RFC 9111 的失效机制是给不安全方法准备的:
A cache MUST invalidate the target URI (Section 7.1 of [HTTP]) when it receives a non-error status code in response to an unsafe request method
静态站部署是「上传一批新文件」,不是对同一个 URI 发一个 PUT,所以不会触发这条(编辑判断)。这意味着:窗口期内的旧副本,只能等它自然陈旧、或者你主动清掉。stale-while-revalidate 会在这段时间里继续把旧的发出去,而且因为它是「先回旧的」,你甚至不会在日志里看到阻塞。
九、出事了,按这个顺序查
「配了还是旧的 / 配了还是报错」有四个不同的原因,混着查会来回打转:
- 响应头里根本没有这两条指令(或写在了请求头、
<meta>、CDN 的控制台里没生效); - 有指令,但窗口已经过了——算一下
Age与max-age + N; - 有指令、窗口也没过,但被第一道闸门压住了——检查
must-revalidate、no-cache、s-maxage、proxy-revalidate; - 都对了,只是这次验证没发生——窗口内没有请求,或者验证本身没触发。
| 步骤 | 看什么 | 判读 |
|---|---|---|
| 1 | 直连源站取响应头 | 没有 stale-* → 先解决「有没有」 |
| 2 | 经过 CDN 再取一次 | 头消失 → 中间层没转发(MUST pass cache directives through) |
| 3 | 响应里的 Age |
有 Age 且大于 max-age → 已经在陈旧区间 |
| 4 | 同一响应里有没有闸门指令 | 有 must-revalidate/no-cache → 两条基本无效 |
| 5 | 窗口内有没有真实请求 | 没有 → 后台验证不会发生 |
| 6 | 验证返回的是什么状态码 | 403/404/429 不在「错误」定义里 |
第 2 步的依据是 RFC 9111 对代理的要求:
A proxy, whether or not it implements a cache, MUST pass cache directives through in forwarded messages, regardless of their significance to that application, since the directives might apply to all recipients along the request/response chain.
regardless of their significance to that application——代理不认识也得原样转发。但「MUST 转发」和「必须照做」是两件事:转发是义务,执行不是。所以「CDN 上有这条头」不能推出「CDN 会照它做」。
十、十分钟核对
先划清工具边界:curl 每次都是全新请求、没有本地缓存,所以它只能告诉你服务器发了什么头,看不到 swr/sie 的真实行为。要观察行为,得用带缓存的代理、或者浏览器的 DevTools(且要区分「memory cache」「disk cache」与 CDN)。
第一组:源站与边缘各发什么头
for u in https://example.com/ https://cdn.example.com/; do
echo "== $u"
curl -sS -o /dev/null -D - "$u" \
| grep -iE '^(HTTP/|cache-control:|age:|date:|etag:)'
done
第二组:把三个数取出来,算总窗口
h=$(curl -sSI https://example.com/ | tr -d '\r' | grep -i '^cache-control:')
echo "$h"
for d in max-age s-maxage stale-while-revalidate stale-if-error; do
printf '%-26s %s\n' "$d" "$(printf '%s' "$h" | grep -oE "$d=[0-9]+" | head -1)"
done
第三组:看闸门指令在不在
curl -sSI https://example.com/ | tr -d '\r' \
| grep -iE '^(cache-control:|pragma:)' \
| grep -oiE 'must-revalidate|no-cache|proxy-revalidate|s-maxage=[0-9]+'
第四组:Age 到底走到哪了
age=$(curl -sSI https://example.com/ | tr -d '\r' | awk -F': ' 'tolower($1)=="age"{print $2}')
maxage=$(curl -sSI https://example.com/ | tr -d '\r' | grep -oiE 'max-age=[0-9]+' | head -1 | cut -d= -f2)
echo "age=${age:-none} max-age=${maxage:-none} 剩余新鲜期=$(( ${maxage:-0} - ${age:-0} ))s"
判读:
| 看到什么 | 说明 | 下一步 |
|---|---|---|
源站有 stale-*、边缘没有 |
中间层没转发或被改写 | 查 CDN 的头改写规则 |
同时有 must-revalidate |
两条都被第一道闸门压住 | 先决定「旧内容能不能用」,再二选一 |
Age 大于 max-age,页面却是新的 |
命中了缓存且内容确实没变 | 正常;要立刻更新就清缓存或换 URL |
只有 stale-while-revalidate、流量很小 |
后台验证大概率不会发生 | 别把它当「自动刷新」 |
只有 stale-if-error、且源站会回 403/429 |
这两个码不在「错误」定义内 | 兜不住,需要别的方案 |
头里 Age 缺失 |
不代表源站生成 | 不能据此判断命中与否 |
十一、上线前十二项清单
- 这个响应「旧多久」是可以接受的吗?先答这个,再谈秒数。
max-age是多少?stale-*的窗口是在它之后另算的。- 两个数加起来的总时长,你能接受吗?(RFC 5861 的 20 分钟例子)
- 同一个响应里有没有
must-revalidate/no-cache/s-maxage/proxy-revalidate? - 如果有,先想清楚要哪一个:要么「旧内容可以顶」,要么「宁可报错」。
- 这个站点在窗口内会有真实请求吗?没有,
swr的后台验证不会发生。 stale-if-error想兜的到底是哪类失败?5xx 之外的码不在定义里。- 响应经过 CDN 后,
Cache-Control里还有这两条吗? Age在边缘是递增的吗?两层是共享一份预算,不是各发一份。- 陈旧的响应会不会带上敏感内容(登录态、余额)?会,就不要用这两条。
- 部署或回滚后,窗口内的旧副本打算怎么处理?换 URL 还是清缓存。
- 上线后人为让源站回一次 502,确认「旧的顶上」与「错误透出」两个分支各出现一次。
六种误读,与本周能做的五件事
| 误读 | 原文怎么说 |
|---|---|
「配了 swr,内容会自动更新」 |
它只允许先回旧的;验证是 SHOULD attempt,且只由窗口内的请求触发 |
「N 秒就是总共能顶多久」 |
窗口从「变陈旧」起算,总时长是 max-age + N;5861 的例子是 600 + 600 = 20 分钟 |
「配了 stale-if-error 源站挂了也不报错」 |
MAY;且错误只指会导致 500/502/503/504 的情形,超出窗口后 SHOULD NOT 再用 |
「陈旧响应一定带 Warning 头」 |
那是 2010 年的要求;RFC 9111 已把 Warning 废弃,只剩 Age 可核 |
「它能绕过 must-revalidate」 |
9111 的第一道闸门在前,must-revalidate 明确 MUST NOT ignore,断网时也要报错 |
| 「RFC 5861 是 HTTP 标准」 | 它是 Category: Informational,原文写明 not an Internet Standards Track specification |
本文不写:各家 CDN、反向代理、框架对这两条指令的支持情况与默认值,缓存命中率与「省了多少请求」的数字,以及任何产品的配置项名称与生效时间。这些都没有一手来源,写出来就是猜。
本周能做的五件事:
- 把站点所有响应里的
stale-*与must-revalidate列出来,看有没有互相打架的组合。 - 挑一个最怕源站抖动的页面,算出它的总窗口(
max-age + N),确认这个时长你能接受。 - 人为让源站对一个测试路径回 502,确认缓存是「顶上」还是「透出错误」。
- 直连源站与经过 CDN 各取一次头,确认两条指令都被转发。
- 把「窗口内的旧副本怎么清」写进发布检查单——部署本身不会让缓存失效。
边界说明
- 本文只引用 RFC 5861、RFC 9111 与 IANA 注册表的原文;每处英文引用都可在对应页面检索到。标注「编辑判断」「编辑推论」的段落是从原文推出来的工程结论,不是规范原文。
- RFC 5861 是 2010 年的 Informational 文档,其中对
Warning头的描述已被 RFC 9111 废弃;两处冲突时以 RFC 9111 为准,本文已在第七节点明。 - 不讨论具体 CDN、反向代理、浏览器对这两条指令的实现差异、支持比例与默认值;这些需要各家官方文档,本文没有取证。
- 不给出「应该配多少秒」的建议:合适的窗口取决于内容变化频率与读者的容忍度,这两个数只有站点自己知道。
- 文中
example.com、cdn.example.com与所有秒数均为占位示例,不代表任何真实站点的配置。
来源
- RFC 5861: HTTP Cache-Control Extensions for Stale Content — https://www.rfc-editor.org/rfc/rfc5861.txt核查于 2026/11/2(2026-11-02 访问;
Category: Informational;§1 引言与两条扩展的定位、§3stale-while-revalidate定义与语法、§3.1 示例与 20 分钟、§4stale-if-error定义/作用域/错误范围/示例、§5 安全考虑与放大攻击) - RFC 9111: HTTP Caching — https://www.rfc-editor.org/rfc/rfc9111.txt核查于 2026/11/2(2026-11-02 访问;§4.2 新鲜与陈旧、§4.2.1 寿命计算与冲突裁决、§4.2.4 发陈旧响应的两道闸门、§4.3.3 验证遇到 5xx 的两种处理、§4.4 失效机制、§5.1
Age、§5.2 指令转发义务、§5.2.2.2must-revalidate、§5.2.2.4no-cache、§5.2.3 扩展指令、§5.5Warning废弃) - IANA: Hypertext Transfer Protocol (HTTP) Cache Directive Registry — https://www.iana.org/assignments/http-cache-directives核查于 2026/11/2(2026-11-02 访问;
stale-while-revalidate→ RFC 5861 §3、stale-if-error→ RFC 5861 §4、max-stale→ RFC 9111 §5.2.1.2)