入门指南
你在请求里写了 no-cache,缓存还是给了旧的:请求端的缓存指令只是建议
同一个 URL,你带了 Cache-Control: no-cache 还是拿到旧副本:RFC 9111 把请求端指令写成 advisory,缓存 MAY 实现;响应端才是 MUST obey。讲清七条请求指令各自在问什么、同名指令为何两侧不同义、max-stale 不带值的含义、no-store 为何是唯一的 MUST NOT 却又不足以保证隐私。
请求端的指令是偏好,响应端的指令才是义务。

先看答案
- 适合谁
- 用 curl 核对发布、给静态站配缓存的人
- 在脚本里加「强制刷新」头,却发现时灵时不灵的人
- 把请求侧的 no-cache 当成「不要存」的人
- 你会获得
- 先看这条指令写在哪一侧,再谈它该不该被听。
- 发布时间
- 实质更新
- 发布
- AOPC 编辑部
60 秒结论
一分钟看懂这篇文章
请求端的指令是偏好,响应端的指令才是义务。
人负责
- 分清「指令被忽略」与「命中了另一层」
- 判断哪些场景只能靠换 URL 解决
AI 负责
- 读请求头里的 Cache-Control
- 比对带与不带时的 Age 与正文
- 确认症状
- 分辨方向
- 查响应侧约束
- 换可验证的手段
直接答案
「部署完了,我在请求里加了 Cache-Control: no-cache,拿到的还是旧页面。」
这句话有两种完全不同的处境,而它们的修法相反:
- 指令被听了,缓存回源验证,源站回了
304 Not Modified——那说明源站认为内容没变,问题在发布这一步,不在缓存。 - 指令没被听,缓存直接把手里那份给你,连源站都没问——这才是本文要讲的:请求端的指令,规范允许缓存不实现。
RFC 9111 用一个词把这件事定死了:
This section defines cache request directives. They are advisory; caches MAY implement them, but are not required to.
而同一个 Cache-Control 头写在响应里,开头换了一句话:
This section defines cache response directives. A cache MUST obey the Cache-Control directives defined in this section.
同一串 no-cache,写在请求里叫 advisory(建议),写在响应里叫 MUST obey(必须遵守)。这条分界线不在指令名上,而在方向上。
| 你发的东西 | 规范给的强度 | 缓存不照做算违规吗 |
|---|---|---|
Cache-Control: max-age=0 |
prefers |
不算——类级声明就是 MAY implement |
Cache-Control: no-cache |
prefers |
不算(但它可能因为别的规则必须回源) |
Cache-Control: max-stale=600 |
客户端表示 will accept |
不算——这是「我接受」,不是「你必须给」 |
Cache-Control: no-store |
MUST NOT store |
算,但它只管「存」,且原文自陈不可靠 |
Cache-Control: no-transform |
asking |
不算;响应侧的 no-transform 才是 MUST NOT |
Cache-Control: only-if-cached |
只对「honor 它」的缓存 SHOULD |
不 honor 就没有义务 |
Pragma: no-cache |
已废弃 | 不该再依赖 |
三句话记住:
- 请求侧七条里,只有
no-store写了MUST NOT,而且它约束的是「存不存」,不是「这一次给你哪一份」; - 其余六条都是
prefers/will accept/asking/SHOULD,缓存可以选择不实现; - 请求指令不清缓存、不删副本、不改寿命,它最多影响「这一次请求拿到哪一份」。
下面按这三点拆开。最后一节给一组能自己跑的核对命令——但先说清它们能证明什么、不能证明什么。
一、指令是单向的
先把一个常见误解拆掉:请求里的指令,不会「传成」响应里的指令。
Cache directives are unidirectional, in that the presence of a directive in a request does not imply that the same directive is present or copied in the response.
你在请求里写 max-age=600,响应不会因此变成 max-age=600;你在请求里写 no-store,也不会让源站在响应里补一个 no-store。两侧各写各的。
但转发是另一回事,代理必须原样带着走:
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. It is not possible to target a directive to a specific cache.
最后半句很关键:你没法只对某一层说话。请求里的 no-cache 会经过浏览器、CDN、反向代理,每一层都看得见,但每一层听不听,由它自己决定。
语法上还有两条容易踩的:
Cache directives are identified by a token, to be compared case-insensitively, and have an optional argument that can use both token and quoted-string syntax.
名字大小写不敏感(NO-CACHE 与 no-cache 是一回事),参数两种写法都要能收。但发的时候,带参数的指令有明确的生成要求,比如:
This directive uses the token form of the argument syntax: e.g., ‘max-age=5’ not ‘max-age=“5”’. A sender MUST NOT generate the quoted-string form.
max-age="5" 这种写法是「发送方不应生成」的形态。收的一方「ought to accept both forms」,所以它通常还能工作,但不要把它写进自己的脚本模板。
数字还有一个边界:delta-seconds 是非负整数,超出实现范围时按 2^31 处理:
If a cache receives a delta-seconds value greater than the greatest integer it can represent, or if any of its subsequent calculations overflows, the cache MUST consider the value to be 2147483648 (2^31) or the greatest positive integer it can conveniently represent.
所以 max-stale=99999999999 不会变成「无限久」,它会先被夹到 2^31 秒(约 68 年)——对「接受任意年龄的陈旧」这个目的来说,差别只在理论上有意义。
二、七条请求指令各自在问什么
RFC 9111 定义七条请求指令。把它们的动词抄在一张表里,强弱一眼可见:
| 指令 | 参数 | 它在请求什么 | 原文用的动词 |
|---|---|---|---|
max-age |
秒 | 年龄不超过 N 秒的响应 | prefers |
max-stale |
秒,可省略 | 接受已超龄的响应;带值=最多超 N 秒,不带值=任意年龄 | will accept |
min-fresh |
秒 | 剩余新鲜期至少还有 N 秒 | prefers |
no-cache |
无 | 用之前先在源站成功验证一次 | prefers |
no-store |
无 | 这次请求与它的响应都别存 | MUST NOT store |
no-transform |
无 | 中间层别转换内容 | asking |
only-if-cached |
无 | 只用已存的;没有就回 504 | SHOULD(仅限 honor 它的缓存) |
「prefers」不是修辞。同一份规范在讲新鲜度计算时又补了一刀:
Clients can send the max-age or min-fresh request directives (Section 5.2.1) to suggest limits on the freshness calculations for the corresponding response. However, caches are not required to honor them.
suggest(建议)与 not required to honor(没有义务照做)——这就是「你带了头,结果没变」最直接的解释,不需要假设任何一家实现有 bug。
三、max-age 与 min-fresh 是筛选条件,不是刷新按钮
max-age 请求侧的定义是「年龄上限」:
The max-age request directive indicates that the client prefers a response whose age is less than or equal to the specified number of seconds. Unless the max-stale request directive is also present, the client does not wish to receive a stale response.
注意后半句:max-age 单独出现时,还隐含「不想要陈旧响应」。所以 max-age=0 的效果是「年龄必须是 0 秒」——听起来很强,其实不然:一份刚刚存进来、还没来得及变旧的副本,年龄就是 0,它满足这个条件(编辑推论,依据是 age <= 0 与新鲜度公式)。
min-fresh 问的是「还能新鲜多久」:
The min-fresh request directive indicates that the client prefers a response whose freshness lifetime is no less than its current age plus the specified time in seconds.
也就是 freshness_lifetime >= current_age + N。它不是「至少存了 N 秒」,方向刚好相反:它要的是剩余时间。
这两条都只影响「这次挑哪一份已存响应」,都不会让缓存去删掉或替换手里那份。它们合起来可以很苛刻(max-age=0, min-fresh=600 基本无法满足),但苛刻不等于强制——回到了第一节那句 not required to honor。
四、max-stale 是「我接受旧的」,不是「你必须给旧的」
这条最容易被读反。原文两句:
The max-stale request directive indicates that the client will accept a response that has exceeded its freshness lifetime. If a value is present, then the client is willing to accept a response that has exceeded its freshness lifetime by no more than the specified number of seconds.
If no value is assigned to max-stale, then the client will accept a stale response of any age.
不带值的 max-stale 是「任意年龄的陈旧我都收」。这看起来像一个大开的闸门,但它只是客户端的许可,不是缓存的授权。缓存能不能给陈旧响应,另有两道闸门:
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).
第一道是「禁止」,第二道是「允许」。顺序是先看有没有被禁止,再看有没有被允许:
| 响应侧写了什么 | 请求带 max-stale |
缓存能回陈旧的吗 |
|---|---|---|
| 没有禁令 | 带了 | 可以——第二道闸门被「客户端明确允许」打开 |
no-cache |
带了 | 不行——第一道闸门先否决 |
must-revalidate |
带了 | 不行,同上 |
s-maxage/proxy-revalidate(共享缓存) |
带了 | 不行,同上 |
stale-while-revalidate |
本来就是陈旧窗口 | 那是另一条扩展的许可,与 max-stale 无关 |
所以「max-stale 能让缓存无视 must-revalidate」是错的: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.
这也解释了一个反直觉的现象:你想让缓存兜住旧内容时,max-stale 不一定帮得上;而源站一句 must-revalidate 就能让它宁可报错。
五、no-cache 请求:这次别用没验证过的
请求侧的定义只有一句,动词仍是 prefers:
The no-cache request directive indicates that the client prefers a stored response not be used to satisfy the request without successful validation on the origin server.
拆开看:它说的是「用之前要成功验证」,不是「别存」。同一个词在响应侧是另一句:
The no-cache response directive, in its unqualified form (without an argument), indicates that the response MUST NOT be used to satisfy any other request without forwarding it for validation and receiving a successful response; see Section 4.3.
一个约束客户端的选择,一个约束缓存的行为;一个 prefers,一个 MUST NOT。这是「同名不同义」里最容易混的一对。
如果缓存真的听了请求侧的 no-cache,接下来发生的是验证,不是替换:
A 304 (Not Modified) response status code indicates that the stored response can be updated and reused; see Section 4.3.4.
A full response (i.e., one containing content) indicates that none of the stored responses nominated in the conditional request are suitable. Instead, the cache MUST use the full response to satisfy the request.
也就是说:源站回 304,缓存把手里那份续期后继续用;源站回 200 带新内容,缓存必须用新的。两条路都不会让缓存「删掉」什么。所以「加 no-cache 把缓存清一遍」这个期待本身就不成立——它最多让这一条路径上的某一层去问一次源站。
六、no-store:唯一的 MUST NOT,但它自己说不够
七条里只有这条在定义句里就写了 MUST NOT:
The no-store request directive indicates that a cache MUST NOT store any part of either this request or any response to it. This directive applies to both private and shared caches.
范围是「这次请求」与「对它的任何响应」,私有缓存和共享缓存都算。接着它把「不存」定义得很具体:
“MUST NOT store” in this context means that the cache MUST NOT intentionally store the information in non-volatile storage and MUST make a best-effort attempt to remove the information from volatile storage as promptly as possible after forwarding it.
「不故意写进非易失存储」+「尽力尽快从易失存储里移除」。注意是 best-effort——它不承诺内存里立刻消失。
然后是最容易被忽略的一句自陈:
This directive is not a reliable or sufficient mechanism for ensuring privacy. In particular, malicious or compromised caches might not recognize or obey this directive, and communications networks might be vulnerable to eavesdropping.
规范自己说它不是保证隐私的可靠或充分机制。把 no-store 当成「这条请求不会留下任何痕迹」的合规凭据,是超出原文的。
还有一条更实际的注记:
Note that if a request containing this directive is satisfied from a cache, the no-store request directive does not apply to the already stored response.
如果这次请求是由缓存满足的,no-store 不适用于那份已经存下的响应。换句话说:你带着 no-store 却吃了一次缓存命中,说明旧副本还在,而且这次命中本身没有违反这条指令。
一处文本张力,按字面读:§5.2.1 的类级声明说请求指令是
advisory、缓存MAY implement,而no-store条目自身写的是MUST NOT store。本文的处理是:类级声明讲的是「这条指令可以被整体不实现」,条目讲的是「一旦实现,存这个动作被禁止」。规范没有为这个张力给裁决句,这里标注为编辑判断,不引申成「所以 no-store 一定生效」。
七、no-transform:请求侧是「请」,响应侧是「必须」
这一对的方向差异最干脆。请求侧:
The no-transform request directive indicates that the client is asking for intermediaries to avoid transforming the content, as defined in Section 7.7 of [HTTP].
asking——请求。响应侧:
The no-transform response directive indicates that an intermediary (regardless of whether it implements a cache) MUST NOT transform the content, as defined in Section 7.7 of [HTTP].
MUST NOT,而且不管它实不实现缓存,只要它是中间层就受约束。RFC 9110 在讲转换的那一节又把它和响应绑了一次:
A proxy MUST NOT transform the content (Section 6.4) of a response message that contains a no-transform cache directive (Section 5.2.2.6 of [CACHING]). Note that this does not apply to message transformations that do not change the content, such as the addition or removal of transfer codings (Section 7 of [HTTP/1.1]).
反过来,没有这条指令时,代理是可以转换的:
A proxy MAY transform the content of a message that does not contain a no-transform cache directive.
所以「我不想让 CDN 压我的图/转我的格式」这件事,在请求里写 no-transform 只是请求;真正有约束力的是源站在响应里写。这也是为什么同一个词在两处出现:一处是客户端的偏好,一处是源站对中间层的禁止。
顺带一句边界:does not apply to message transformations that do not change the content——增删 transfer coding 这类不改内容的处理,不在禁令范围内。
八、only-if-cached:宁可 504,也不回源
The only-if-cached request directive indicates that the client only wishes to obtain a stored response. Caches that honor this request directive SHOULD, upon receiving it, respond with either a stored response consistent with the other constraints of the request or a 504 (Gateway Timeout) status code.
三个限定词都要读到:
Caches that honor this request directive——前提是它愿意听;SHOULD——不是MUST;consistent with the other constraints of the request——还得满足max-age之类其它条件,不满足就落到 504。
504 本身的定义是这样的:
The 504 (Gateway Timeout) status code indicates that the server, while acting as a gateway or proxy, did not receive a timely response from an upstream server it needed to access in order to complete the request.
注意它描述的是「没从上游及时收到响应」。而 only-if-cached 的场景里,缓存根本没有去问上游。所以这里的 504 是一个被复用的状态码,语义上并不完全贴合(编辑判断)。实践含义是:看到 504 不要立刻去查上游健康,先看请求里有没有 only-if-cached。
这条指令真正的用途是 离线或弱网时只读已存内容:宁可失败,也不要发一个可能很慢的请求。它不适合用来「探缓存有没有这份东西」——没命中时你拿到的是 504,而不是一个能区分「没存」与「存了但不合适」的答案。
九、Pragma 已废弃,且响应侧含义从未被规定
Pragma: no-cache
这行头比 Cache-Control 老。规范把来历和现状写在一起:
The “Pragma” request header field was defined for HTTP/1.0 caches, so that clients could specify a “no-cache” request (as Cache-Control was not defined until HTTP/1.1).
However, support for Cache-Control is now widespread. As a result, this specification deprecates Pragma.
deprecates——废弃,不是删除。它仍然可能被老链路识别,但新代码没有理由只靠它。
更需要注意的是响应侧:
Because the meaning of “Pragma: no-cache” in responses was never specified, it does not provide a reliable replacement for “Cache-Control: no-cache” in them.
「响应里的 Pragma: no-cache 含义从未被规定」。所以它不能当 Cache-Control: no-cache 的替代品,也不能拿来推断响应会被怎么处理。这一句在排查时很有用:如果你在一个响应里看到孤零零的 Pragma: no-cache,那不是一个可依赖的信号,最多是某个老组件留下的痕迹。
十、两个人两个结果:四种解释
「我这儿是新的,他那儿是旧的」不一定是谁错了。按证据强弱排,有四种:
| 解释 | 机制 | 能拿到的证据 | 下一步 |
|---|---|---|---|
1. 一个人发了 no-cache,另一个人没发 |
前者触发验证,后者命中新鲜副本 | 两人请求头不同;Age 有无 |
这不是故障,是两种请求 |
| 2. 指令被忽略 | §5.2.1 允许缓存不实现 | 同一请求头重复多次,结果仍固定为旧副本 | 换 URL、purge,别加更多头 |
| 3. 两人命中不同层 | 一个命中浏览器,一个命中 CDN 边缘 | Age 不同;有 Cache-Status 时逐层可见 |
按层定位(见另篇) |
| 4. 两人请求的不是同一个 URL | 尾斜杠、?v=、www 与非 www 是不同缓存键 |
对比完整 URL 与重定向链 | 统一规范化 |
只有第 2 种是「指令没被听」。第 1 种是最常见的误判:把「他没发 no-cache」当成「缓存坏了」。
第 3 种可以用 Age 分辨。它的含义是:
The “Age” response header field conveys the sender’s estimate of the time since the response was generated or successfully validated at the origin server.
以及一条很容易读反的注记:
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」不能当「回源了」的证据。
十一、十分钟核对
先划清边界:curl 自己没有缓存,每次都是全新请求。所以下面这组命令测的是路径上的缓存听不听,不是「本机缓存有没有生效」。
第一组:同一 URL,三种请求头,比较状态、Age 与正文
u=https://example.com/
for h in "X-None: 1" "Cache-Control: no-cache" "Cache-Control: max-age=0"; do
echo "== $h"
curl -sS -o /tmp/body.$$ -D /tmp/hdr.$$ -H "$h" "$u"
sed -n '1p' /tmp/hdr.$$
grep -iE '^(age|date|cache-control|etag):' /tmp/hdr.$$
shasum -a 256 /tmp/body.$$ | cut -c1-16
done
rm -f /tmp/body.$$ /tmp/hdr.$$
第二组:响应侧到底有没有禁令(这决定缓存能不能给你旧的)
curl -sS -o /dev/null -D - "$u" | grep -iE '^(cache-control|expires|age):'
第三组:验证是不是真的发生了
etag=$(curl -sS -o /dev/null -D - "$u" | grep -i '^etag:' | tr -d '\r' | cut -d' ' -f2-)
curl -sS -o /dev/null -D - -H "If-None-Match: $etag" "$u" | sed -n '1p'
判读:
| 看到什么 | 说明 | 下一步 |
|---|---|---|
三种请求头返回的正文哈希全相同,Age 也相同 |
缓存没区分这三种请求 | 可能忽略了请求指令;换 URL 或 purge |
带 no-cache 时 Age 消失或变小 |
这一层去验证了 | 正常;再看源站给的是 304 还是 200 |
带 no-cache 仍返回 Age: 1800 |
命中了已存副本,且没回源 | 这一层没听;别再加头,换手段 |
响应里有 no-cache/must-revalidate |
缓存本来就不该给陈旧副本 | 旧副本来自更外层或更早的副本 |
第三组回 304 |
校验器工作正常 | 源站认为内容没变,去查发布 |
第三组回 200 |
校验器对不上 | 内容确实变了,缓存会更新 |
第二组要特别看 no-cache 与 must-revalidate:有它们,缓存不能给你陈旧副本(第一节引的两条 MUST NOT)。这时候「我看到旧的」更可能是命中了另一层,或者那一层存的是更早的响应。
十二、上线前十二项清单
- 你加的
no-cache是加在请求里,还是响应里?两者的规范强度不同。 - 这次核对的目标是「让某一层去问源站」,还是「让所有层都丢掉旧副本」?后者不能靠请求头。
- 脚本里发的是
max-age=0还是no-cache?它们问的不是一件事。 - 带参数的指令用了 token 形式(
max-age=0),而不是max-age="0"? max-stale不带值时,你知道它等于「接受任意年龄的陈旧」吗?- 需要缓存兜住旧内容时,响应侧有没有
must-revalidate把这条路堵死? no-store是不是被当成了「清除已有副本」?它明确不适用于已存响应。- 有没有把
no-store当成隐私保证?原文自陈不足。 - 想禁止中间层转换内容时,
no-transform写在请求里还是响应里? only-if-cached收到 504 时,会不会误判成上游超时?- 还在发
Pragma: no-cache吗?现代缓存看的是Cache-Control。 - 核对脚本比较的是正文哈希,还是只看状态码?
200不代表内容是你以为的那份。
七种误读
| 误读 | 原文怎么说 |
|---|---|
| 「请求指令一定生效」 | They are advisory; caches MAY implement them, but are not required to. |
「max-age=0 等于 no-cache」 |
一个限制年龄上限,一个要求成功验证;动词都是 prefers,但问的不是一件事 |
「no-cache 是不许存」 |
请求侧是「用前先验证」;响应侧才是 MUST NOT be used ... without ... validation |
「no-store 能保证隐私」 |
This directive is not a reliable or sufficient mechanism for ensuring privacy. |
「only-if-cached 一定回 504」 |
SHOULD,且只对 Caches that honor this request directive 成立 |
「max-stale 能让缓存无视 must-revalidate」 |
In all circumstances, a cache MUST NOT ignore the must-revalidate directive |
「Pragma: no-cache 等价于 Cache-Control: no-cache」 |
已废弃;响应侧语义 was never specified |
本文不写:各家浏览器、CDN、反向代理对请求指令的实现差异与支持比例,它们各自的重载行为,以及任何命中率或耗时数字。这些都需要各家官方文档或实测数据,本文没有取证。
本周能做的五件事:
- 把发布核对脚本里的「强制刷新」头改成两种分别测:
no-cache与max-age=0,看结果是否不同。 - 给核对脚本加一步「正文哈希」,不要只看状态码。
- 找一条线上响应,确认它有没有
must-revalidate——这决定陈旧副本能不能被兜住。 - 把「要让所有层更新,只能换 URL 或 purge」写进发布检查单。
- 清掉代码里只发
Pragma: no-cache的地方,换成Cache-Control。
边界说明
- 本文只引用 RFC 9111、RFC 9110 与 IANA 缓存指令注册表的原文;每处英文引用都可在对应页面检索到。标注「编辑判断」「编辑推论」的段落是从原文推出的工程结论,不是规范原文。
- 不讨论具体浏览器、CDN、反向代理对请求指令的实现差异与支持比例;这些需要各家官方文档,本文没有取证。
- 不讨论
Cache-Control与Vary、ETag的配合细节;那些在相邻的几篇里分别处理。 - 文中
example.com为占位示例,不代表任何真实站点的配置。
来源
- RFC 9111: HTTP Caching — https://www.rfc-editor.org/rfc/rfc9111.txt核查于 2026/11/3(2026-11-03 访问;§4.2.1 新鲜度计算与「无义务照做」、§4.2.4 两道陈旧闸门、§4.3.3 验证响应处理、§5.1
Age、§5.2 指令通用规则与单向性、§5.2.1 七条请求指令、§5.2.2 响应指令开头、§5.2.2.2must-revalidate、§5.2.2.4 响应no-cache、§5.2.2.5 响应no-store、§5.2.2.6 响应no-transform、§5.4Pragma) - RFC 9110: HTTP Semantics — https://www.rfc-editor.org/rfc/rfc9110.txt核查于 2026/11/3(2026-11-03 访问;§7.7 消息转换与
no-transform的MUST NOT、§15.6.5504 Gateway Timeout定义) - IANA: Hypertext Transfer Protocol (HTTP) Cache Directive Registry — https://www.iana.org/assignments/http-cache-directives核查于 2026/11/3(2026-11-03 访问;
max-age/no-cache/no-store/no-transform各指向请求与响应两节,max-stale/min-fresh/only-if-cached只有请求侧一节)