Vercel Cache Reason:相同 MISS,怎样决定下一步查什么
设想一个定价页已经更新,访问者拿到的仍是旧价格。请求返回 200,全站缓存命中率也没有明显下降,值班人员却无法回答:新内容到底没有生成、没有进入缓存,还是重验证一直失败?如果只把 MISS 当成"回源"、把 200 当成"正常",这些路径会混进同一张绿色看板。
本文把这个假设场景拆成一个可运行的日志分流器。它保留状态、原因和缺失证据,给出下一项检查;最终根因仍要靠配置、请求关联和源站记录确认。示例使用作者构造的有限事件,实际运行的结果只证明这些分支的本地行为,不能证明某个站点已经发生或解决了上述故障。

先确认看的是哪一层缓存
Vercel 在 2026 年 7 月 17 日的公告中加入 Runtime Logs Cache Reason,让可被 CDN 缓存的响应在状态之外显示原因。公告覆盖 ISR、Partial Prerendering 和带相关 Cache-Control 指令的函数响应;每次请求动态渲染的响应可能没有原因。适用范围决定了空值如何解释,不能先把没有 Reason 的事件都归为采集失败。官方公告
还要区分响应缓存和函数内部的数据缓存。x-vercel-cache 说明 CDN 对响应的处理;动态路由的函数即使从 Runtime Cache 取到了数据,外层响应仍可能显示 MISS。因此,从一个 HTTP 响应头不能推出"所有缓存层都未命中",也不能据此计算函数里的数据库访问次数。响应头文档
一次请求至少涉及三个独立问题:访问者收到了什么,CDN 怎样取得这个响应,源站或后台重生成做了什么。响应状态回答第一类问题,Cache Status 与 Reason帮助第二类问题,函数日志和后台任务记录帮助第三类问题。把它们保存成不同字段,后面才能表达"访问者收到旧内容且返回成功,后台生成失败"这种同时成立的情况。
这也解释了为何 Cold 不能翻译成"应用冷启动"。它是缓存里没有可供使用的内容,不是运行容器刚启动的证明。新部署、不同地域、低频对象被逐出,都会给排查带来不同方向;是否发生函数冷启动,仍要看函数自己的启动信息,不能拿两个都包含"冷"的概念互相替代。

应用侧扩展事件示例
下面给出一份完整推荐记录,全部是作者设计的应用侧归一化字段,不是 Vercel 原生导出 schema。时间、请求ID、路径、地域和数值都是示例;采集时缺失的字段应保持缺失,不能凭空填充。origin_latency_ms 与 response_status 分别来自关联的源站/响应观测,不能由一个 Cache Reason 推出。
json
{
"timestamp": "2026-07-18T10:20:30Z",
"request_id": "req_...",
"url": "/tools/json-formatter",
"route_type": "isr",
"region": "sfo1",
"user_agent_class": "human|search_crawler|ai_crawler|other_bot",
"cache_status": "STALE",
"cache_reason": "Revalidation error",
"origin_latency_ms": 830,
"response_status": 200,
"response_size_bytes": 218044,
"deployment_id": "dpl_...",
"content_digest": "sha256:..."
}
content_digest 建议由应用生成;user_agent_class 来自作者的分类规则,均不声称 Vercel 自动提供。真正接入前先记录原始事件字段,再显式映射这些扩展字段。
用 Status 与 Reason 的组合选检查方向
下面这张表以 2026 年 10 月 14 日核对的Cache Status and Reasons为语义入口,第三列是本文建议的检查动作。动作表示排查方向,不表示单条日志已经证明原因链的每一环。
| Status | Reason | 下一项检查与判断边界 |
|---|---|---|
| MISS | Cold | 对比部署、地域、路由和缓存键。首次生成可以正常,持续异常需要时间与流量基线。 |
| MISS | Request collapsed | 查看共享生成的等待时间。等待请求没有各自命中已存对象,不代表每个请求都独立回源。 |
| MISS | Error | 关联缓存读失败与源站记录。这个原因指缓存存储读取失败,不能直接等同应用 5xx。 |
| MISS | Vary key denied | 检查 Vary 与个性化响应。不要为提高命中率而移除仍有语义作用的请求维度。 |
| BYPASS | Draft Mode | 确认草稿预览条件是否符合访问者身份和路由用途。 |
| BYPASS | Prerender Bypass | 核对框架生成的路由绕过配置,不能仅凭用户代理名称推断具体分支。 |
| BYPASS | Crawler | 确认是否等待完整页面,以及等待延迟是否符合爬虫访问目标。 |
| STALE | Time-based revalidation | 检查重生成进度与内容年龄,过期响应被快速返回不代表新版本已完成。 |
| STALE | Tag-based invalidation | 关联标签失效事件与后台更新,核对下一次内容版本。 |
| STALE | Revalidation error | 查重生成失败,结合业务允许的旧内容时长评估影响。 |
| REVALIDATED | Tag-based deletion | 查标签删除与前台重新生成耗时,不能把它当成后台刷新。 |
| HIT / PRERENDER | 无需强行补原因 | 分别表示缓存返回或静态存储返回。没有 Reason 本身不构成错误。 |
原来的七月公告只有十项带原因的组合,当前参考额外列出 Vary key denied,并给出原始原因例子 vary_key_denied:cookie。七月公告是历史分类,不能作为当前枚举全集;新采集器应保存原始字符串,遇到新增值进入未识别分支,等待复核后更新映射。把未知值自动映成 Cold,会让平台新增能力悄悄变成错误统计。
证据图只摘录 Cold、Request collapsed、Error 三个官方原因字段,并把作者建议标明;它不是原因枚举全集,现行分类以上表和官方参考为准。
Request collapsed 值得单独理解:一些请求等待同一份正在生成的响应,它们没有从现成缓存里取到对象,因此表现为 MISS。这个分支通常能减少重复生成,但依然可能因为生成慢而让访问者久等。把 MISS 数直接换算成函数调用数,会把等待共享结果的请求重复计费;是否节约调用,应关联实际调用记录,而不是从状态计数推算。
同样都是与标签相关的更新,Tag-based invalidation 与 Tag-based deletion 也不能合并成一个"刷新"。前者仍可先返回现有副本并后台生成,后者删除后需要前台生成,访问者等待路径不同。排障时若把两者都写成"触发重验证",就会丢失为什么这一批请求的延迟突然升高的线索。
一条归一化事件要保留两种未知
最小输入应保留请求标识、部署、地域、响应状态以及状态与原因。真实导出字段的命名和嵌套位置必须通过当前 CLI 与账户样本确认。本文的 cache_status、cache_reason、origin_status 等蛇形字段是作者定义的归一化格式,不是承诺 Vercel 会直接导出这个 JSON Schema。
第一种未知是"字段没有采到"。例如日志只有响应状态,没有后台生成的 HTTP 状态,此时 origin_5xx 应当是未知,而不能补成 false。HIT 请求可能根本没有此次回源,把 origin_status 标成缺少证据也不表示源站故障;它只是提醒使用者不能从这一条事件推出源站健康结论。
第二种未知是"字段采到了,但当前规则不理解"。例如出现 Future reason,或 HIT 与 Cold 被归在一起。前者可能是平台新增枚举,后者也可能是适配器把不同请求拼错。保留原始事件、映射版本与导出范围,才能区分两种情况。直接丢弃未知行,表面上会让已识别比例更好看,却同时删掉最需要追踪的变化。
内容年龄和允许旧内容时长也需要独立来源。示例里的 content_age_s、max_age_s 是业务侧提供的整数秒,不假设它们由 Cache Reason 自动产生。缓存对象的存储时长、业务数据更新时间和最后一次成功重生成时间可能不同;定价数据应选择与价格生效相关的时间,不应直接把日志发生时间减部署时间当成内容年龄。
如果希望比较普通访问者、搜索爬虫和 AI 爬虫拿到的页面版本,可以另外由应用生成 content_digest。摘要对象应明确,比如正文业务数据加版本号;随机请求标识、个性化推荐或动态时间戳应在比较前区分。不同摘要只提示差异,需要继续检查语言、权限、实验分组、Canonical、noindex 与主体内容。content_digest 是本文建议的应用字段,既不是 Vercel 原生字段,也不能单独证明搜索排名损失。


保存成一个文件,直接运行有限事件
下面的 Python 程序只用标准库,不请求网络、不连接 Vercel 账户。保存为 cache_triage.py,用 Python 3 运行即可。它包含十二个构造事件和十二条边界断言,覆盖缓存读取错误但源站成功、200 旧响应而后台失败、合法空原因、缺原因、新原因、错误配对、新增 Vary 原因和类型异常。
python
import json
# Author-defined normalized events, not a Vercel export schema or live sample.
RULES = {
("MISS", "Cold"): ("cold", "compare_deployment_region_and_cache_key"),
("MISS", "Request collapsed"): ("collapsed", "inspect_shared_generation_wait"),
("MISS", "Error"): ("cache_read_error", "correlate_cache_read_and_origin_logs"),
("MISS", "Vary key denied"): ("vary_denied", "inspect_vary_and_personalization"),
("BYPASS", "Draft Mode"): ("draft_bypass", "check_preview_cookie_and_route_token"),
("BYPASS", "Prerender Bypass"): ("prerender_bypass", "check_route_bypass_configuration"),
("BYPASS", "Crawler"): ("crawler_bypass", "check_fallback_and_crawler_timing"),
("STALE", "Time-based revalidation"): ("timed_stale", "check_refresh_and_content_age"),
("STALE", "Tag-based invalidation"): ("tag_stale", "correlate_tag_event_and_refresh"),
("STALE", "Revalidation error"): ("refresh_error", "inspect_regeneration_failure"),
("REVALIDATED", "Tag-based deletion"): ("tag_deleted", "inspect_deletion_and_foreground_latency"),
}
STATUSES = {"HIT", "MISS", "BYPASS", "STALE", "PRERENDER", "REVALIDATED"}
def triage(event):
status, reason = event.get("cache_status"), event.get("cache_reason")
missing = [k for k in ("request_id", "region", "deployment_id")
if not isinstance(event.get(k), str) or not event[k]]
http = event.get("response_status")
origin = event.get("origin_status")
valid_http = type(http) is int and 100 <= http <= 599
valid_origin = type(origin) is int and 100 <= origin <= 599
if not valid_http:
missing.append("response_status")
if not valid_origin:
missing.append("origin_status")
if not isinstance(status, str) or status not in STATUSES:
bucket, check = "unknown_status", "inspect_raw_event_and_adapter"
elif reason is None or reason == "":
if status in {"HIT", "PRERENDER"}:
bucket, check = "served_without_reason", "compare_content_version_if_needed"
elif status == "MISS" and event.get("cacheable") is False:
bucket, check = "dynamic_without_reason", "check_declared_rendering_strategy"
else:
bucket, check = "missing_reason", "check_route_scope_and_export_fields"
missing.append("cache_reason")
else:
normalized = reason
if isinstance(reason, str) and reason.startswith("vary_key_denied:") and reason.split(":", 1)[1]:
normalized = "Vary key denied"
if not isinstance(normalized, str) or (status, normalized) not in RULES:
bucket, check = "unknown_pair", "preserve_raw_reason_and_update_mapping"
else:
bucket, check = RULES[status, normalized]
expired = None
if status == "STALE":
age, budget = event.get("content_age_s"), event.get("max_age_s")
if type(age) is int and age >= 0 and type(budget) is int and budget >= 0:
expired = age > budget
else:
missing.append("content_age_s/max_age_s")
return {"bucket": bucket, "next_check": check,
"response_5xx": 500 <= http <= 599 if valid_http else None,
"origin_5xx": 500 <= origin <= 599 if valid_origin else None,
"content_over_budget": expired, "missing": missing}
def sample(status, reason, **extra):
return dict(request_id="fixture", region="demo", deployment_id="fixture-v1",
response_status=200, cache_status=status, cache_reason=reason, **extra)
CASES = [
("cold", sample("MISS", "Cold", origin_status=200)),
("collapsed", sample("MISS", "Request collapsed", origin_status=200)),
("cache-error-200", sample("MISS", "Error", origin_status=200)),
("stale-200", sample("STALE", "Revalidation error", origin_status=503,
content_age_s=3600, max_age_s=300)),
("hit-no-reason", sample("HIT", None)),
("dynamic-no-reason", sample("MISS", None, cacheable=False)),
("cacheable-no-reason", sample("MISS", None, cacheable=True)),
("new-reason", sample("MISS", "Future reason")),
("wrong-pair", sample("HIT", "Cold")),
("vary-cookie", sample("MISS", "vary_key_denied:cookie", origin_status=200)),
("unknown-status", sample("FUTURE", "Cold")),
("bad-types", sample(["MISS"], {"reason": "Cold"}, origin_status=True)),
]
if __name__ == "__main__":
results = {name: triage(event) for name, event in CASES}
assert results["cache-error-200"]["origin_5xx"] is False
assert results["stale-200"]["response_5xx"] is False
assert results["stale-200"]["origin_5xx"] is True
assert results["stale-200"]["content_over_budget"] is True
assert results["hit-no-reason"]["bucket"] == "served_without_reason"
assert results["dynamic-no-reason"]["bucket"] == "dynamic_without_reason"
assert results["cacheable-no-reason"]["bucket"] == "missing_reason"
assert results["new-reason"]["bucket"] == "unknown_pair"
assert results["wrong-pair"]["bucket"] == "unknown_pair"
assert results["vary-cookie"]["bucket"] == "vary_denied"
assert results["bad-types"]["origin_5xx"] is None
assert results["unknown-status"]["bucket"] == "unknown_status"
for name, result in results.items():
print(name + " " + json.dumps(result, ensure_ascii=False, sort_keys=True))
print("PASS 12 events; 12 boundary assertions; synthetic normalized fixtures only")
bash
python3 cache_triage.py
本次实际运行使用 CPython 3.11.6、macOS ARM64,开始时间为 2026-10-14 11:03:05 UTC,退出码为 0,标准错误为空。源文件 SHA-256 为 f82f6652cb19bc1f6f8aedde0f04ba125305b1c0397c8bc9edb01eb382a82994。以下为实际标准输出,没有将手写预期结果充当运行记录。
text
cold {"bucket": "cold", "content_over_budget": null, "missing": [], "next_check": "compare_deployment_region_and_cache_key", "origin_5xx": false, "response_5xx": false}
collapsed {"bucket": "collapsed", "content_over_budget": null, "missing": [], "next_check": "inspect_shared_generation_wait", "origin_5xx": false, "response_5xx": false}
cache-error-200 {"bucket": "cache_read_error", "content_over_budget": null, "missing": [], "next_check": "correlate_cache_read_and_origin_logs", "origin_5xx": false, "response_5xx": false}
stale-200 {"bucket": "refresh_error", "content_over_budget": true, "missing": [], "next_check": "inspect_regeneration_failure", "origin_5xx": true, "response_5xx": false}
hit-no-reason {"bucket": "served_without_reason", "content_over_budget": null, "missing": ["origin_status"], "next_check": "compare_content_version_if_needed", "origin_5xx": null, "response_5xx": false}
dynamic-no-reason {"bucket": "dynamic_without_reason", "content_over_budget": null, "missing": ["origin_status"], "next_check": "check_declared_rendering_strategy", "origin_5xx": null, "response_5xx": false}
cacheable-no-reason {"bucket": "missing_reason", "content_over_budget": null, "missing": ["origin_status", "cache_reason"], "next_check": "check_route_scope_and_export_fields", "origin_5xx": null, "response_5xx": false}
new-reason {"bucket": "unknown_pair", "content_over_budget": null, "missing": ["origin_status"], "next_check": "preserve_raw_reason_and_update_mapping", "origin_5xx": null, "response_5xx": false}
wrong-pair {"bucket": "unknown_pair", "content_over_budget": null, "missing": ["origin_status"], "next_check": "preserve_raw_reason_and_update_mapping", "origin_5xx": null, "response_5xx": false}
vary-cookie {"bucket": "vary_denied", "content_over_budget": null, "missing": [], "next_check": "inspect_vary_and_personalization", "origin_5xx": false, "response_5xx": false}
unknown-status {"bucket": "unknown_status", "content_over_budget": null, "missing": ["origin_status"], "next_check": "inspect_raw_event_and_adapter", "origin_5xx": null, "response_5xx": false}
bad-types {"bucket": "unknown_status", "content_over_budget": null, "missing": ["origin_status"], "next_check": "inspect_raw_event_and_adapter", "origin_5xx": null, "response_5xx": false}
PASS 12 events; 12 boundary assertions; synthetic normalized fixtures only
cache-error-200 被分到 cache_read_error,origin_5xx 为 false。这说明程序没有因为原因文字含有 Error 就把源站记成失败。stale-200 的 response_5xx 为 false,origin_5xx 为 true,content_over_budget 为 true,说明成功响应、后台错误与内容超时可以同时被保留;构造的 3600 秒内容年龄和 300 秒预算只用于展示分支,不能当成线上告警推荐值。
hit-no-reason 与 dynamic-no-reason 分别得到 served_without_reason 和 dynamic_without_reason。后者需要输入明确声明 cacheable=False,程序不会从"原因为空"反推动态渲染。cacheable-no-reason 留在 missing_reason,它提示核对路由范围和导出字段,而不宣布 CDN 损坏。new-reason、wrong-pair 保留未知组合;vary-cookie 才能进入已核对的新增分支。
程序刻意没有把原因归类直接变成重启、清缓存或下架动作。它输出 next_check 与 missing,使调用者先补证据。类型检查也让布尔值 true 不会被 Python 当作整数 HTTP 状态,列表形式的状态和对象形式的原因不会触发哈希异常。真实 JSONL 接入时,还需要给解析失败行单独留档;这份有限示例不包含导出适配器、认证、批量读取、采样校正或生产告警服务。
十二条断言只覆盖给定边界,不等于验证了所有组合。接入实际导出后,应先冻结一份脱敏原始样本,记录字段路径,再补自己的框架和 CLI 版本对应的映射。特别是缺少后台错误记录的 STALE,仍可根据 Reason 给出检查方向,但 origin_5xx 保持未知,不能为了让看板整齐而填零。
用一条请求证据完成排障
回到开头的旧价格页面。第一步取具体请求,而不是刷新全站 HIT Rate。保存环境、部署标识、地域、URL、UTC 时间、响应状态、状态与原因,并确认记录来自需要排查的生产部署。预览域名或历史部署的数据即使字段完整,也不能拿来证明当前公开页面异常。
如果是 MISS / Cold,先比较问题发生前后的部署和地域。新部署后第一次生成,与同一部署同一路由在同一地域反复冷缺失,是不同现象。后者需要继续检查缓存键是否变化、响应是否仍可缓存、对象是否被逐出,以及低请求量能否支持所谓"反复"的判断。没有这些关联,延长缓存时间只是一个待验证的处理选项。
如果是 MISS / Request collapsed,沿请求时间找到共享生成的函数执行,再核对等待时长。示例中的 collapsed 只给这一方向,并未计算真实节省量。一次生成慢会让许多等待请求一起慢;应先查看生成中的数据库、内容服务或模型调用,而不能因为 MISS 多就立即扩大函数并发,反而给相同上游增加压力。
如果是 MISS / Error,保留缓存读故障与源站执行两条记录。源站 200 时,访问者可能仍然拿到新响应,问题主要体现为回源压力和延迟;源站同时失败时,影响范围可能扩大,但相关性仍不足以证明缓存错误导致应用报错。需要相同时间、地域、部署和请求关联才能收敛,不能用全站两条同时上升的曲线替代关联证据。
如果是 STALE / Revalidation error,读取最后成功内容版本与失败的生成记录。旧价格能否继续服务由业务允许时长决定,不由返回 200 决定。应核对新版本为何没有生成,再检查源站可达性、函数错误和内容服务更新;如果业务要求立即一致,需要评估当前缓存策略是否满足这个条件,而不是无限依赖最后一个可用副本。
BYPASS 则先验证绕过意图。Draft Mode 需要看预览 Cookie 与路由条件,生产域名上的编辑预览可能合法;公开流量不应携带的预览条件扩散才值得处理。Prerender Bypass 对应路由配置,Crawler 对应平台识别并等待完整页面的路径,它们不能简单按"爬虫"合并。检查中要保留配置依据,避免为了减少回源把爬虫重新导向未完成的页面壳。
Vary key denied 要先判断内容是否真的按 Cookie 等请求头变化。如果变化是必要的个性化,公共缓存就不是合适目标;如果响应并不依赖该维度,才考虑修正多余的 Vary。只看命中率的优化容易把这个顺序倒置,省下几个缓存对象却让不同用户共享不该共享的响应。最终处理仍要回到响应语义和应用配置。
看板与告警跟着证据范围走
已有全站命中率可以保留为趋势入口,但下一屏应先按页面类型、部署和地域切开,再看 Status 与 Reason 分布。高流量静态资源可能掩盖低流量的核心工具或定价页面;一个地域的 Cold 也可能被其他地域的 HIT 稀释。建议同时显示每组请求数、观察时间范围和未知字段数,让使用者知道比例是否来自足够样本。
分流后的面板可以分成四类职责:高价值路由的原因时间线;地域级 Cold 与源站延迟;不同访问者类别的状态及内容版本差异;缓存读错误、重验证失败和不符合预期的绕过事件列表。它们都服务于同一个问题------这一条异常下一步查哪里,而不需要为每一种原因再建立一套独立指标体系。
源站延迟也要注明度量对象。全请求响应延迟的 P95 不等于实际函数生成延迟的 P95;Request collapsed 等待请求与触发生成的请求,应按需要分别分析。示例没有延迟字段,因此不能输出延迟结论。真实落地时需要把函数执行耗时、共享等待耗时和访问者端总耗时的来源写清,缺任何一个就保留对应未知。
告警等级由业务影响和持续时间决定。可把核心页面超过内容新鲜度预算、确认主体或搜索元信息出现不符合预期的版本差异、缓存读错误伴随源站失败与延迟超标,作为高优先级候选。生产公开流量持续出现非预期 Draft Mode、某地域 Cold 明显偏离基线、爬虫绕过产生额外延迟和回源,可进入次一级候选;非核心路由的短时 Cold 或延迟仍达标的请求合并通常适合观察。这里的等级是工程建议,不是厂商定义的故障标准。
原方案里的"Cold 比基线高三倍"也只能作为待校准候选。三次请求里一次缺失,与三万次请求里一万次缺失,即使比例相同,业务影响和统计稳定性都不同。阈值还需要最小请求数、持续窗口、发布后的宽限条件和页面价值。部署切换后的预期变化不应与稳定期用同一阈值,未知原因的增长则适合进入数据质量检查,而不是静默从分母剔除。
从 CLI 取样前,先确认可用条件
官方公告给出了按请求查看原因和按原因聚合的 CLI 入口。公告中的历史命令与当前 CLI 不能混用:本轮核验的 logs 文档将新入口限定为 v64.0.0 及以上,已移除 --request-id、--expand 和 --branch。以下是当前文档的调用形态,TEAM、PROJECT 和 REQUEST_ID 必须替换为自己的可访问对象;本文没有执行这些账户级命令,也没有真实 CDN 请求样本。先记录已安装 CLI 版本与命令帮助,再依据当前账户的 schema 核对可查询指标和维度。Cache Reason 公告
bash
vercel --version
vercel logs --help
vercel logs --query 'requestId:"REQUEST_ID"' --json --project PROJECT --scope TEAM
vercel logs requests --help
vercel metrics schema vercel.request.count --project PROJECT --scope TEAM
vercel metrics vercel.request.count --group-by cache_reason --since 1h --project PROJECT --scope TEAM
当前logs 命令参考说明,静态资源和 CDN 请求使用 vercel logs requests 入口,具体过滤项先查该子命令帮助。JSON 输出是 JSON Lines,末行的 summary 是查询摘要,不是请求事件;其中的 recordsReturned、timeRange、pagination 和 excludedIntervals 应单独保留,不能进入请求计数。默认能返回若干事件不等于全量流量,因此取样后应检查项目、部署、环境、时间窗口、返回条数与分页。后续归一化必须依据实际导出,不能假定每行都与本文 fixture 一样。
metrics 命令参考要求先用 schema 确认账户可查询的指标、聚合和维度;除 Web Analytics 和 Speed Insights 的例外,观测指标查询需要相应套餐的 Observability Plus。没有权限或维度不匹配时,保留失败信息并改用可访问的日志检查,不要把空结果解释成"没有异常",也不要为完成本文示例去改团队权限。
Runtime Logs 文档给出不同套餐的保留期。选定一小时作为 CLI 查询窗口,不会使账户拥有本来已过期的历史数据。排障开始时应先核对可用保留范围;分析导出数据时再记录导出上限、是否触顶、实际覆盖窗口以及是否存在采样。若没有确认覆盖率,就只能说"这份导出里有多少",不能把它扩展为整站错误率或完整账单。

修复后,用同一证据链验收
修复一次缓存配置,验收对象应固定为同一类路由、目标部署与访问类别。内容正确性要看新版本或业务摘要,用户体验要看相同口径的延迟,生成路径要看对应 Reason 和源站结果。降低 MISS 但继续返回错误旧价格,不能算修复完成;返回新价格却让所有请求都回源,也只能说明内容恢复,需要继续评估缓存策略的代价。
如果要把这个产物用于 AscendLab 一类应用站点,最小方案可以是导入脱敏日志、在本地浏览器聚合,再关联页面价值 CSV。原始请求中的 Cookie、查询参数和可识别访问者信息应在导出或处理时按需要减少;"浏览器本地处理"也不自动等于所有数据都安全。这里是可选产品方向,没有声称已经接入该站点、验证线上收益或建立生产告警。
最终可带走的是这个保留未知值的分流器和它要求的关联证据。先让异常请求明确指向一个下一步检查,再补齐这一步需要的配置、版本或源站记录;只有证据闭合后才决定清缓存、调整路由或修复生成任务。这样,状态统计能保留汇总价值,原因字段也真正进入可复查的排障过程。
参考资料
- Runtime logs now show cache reasons,2026-07-17 公告。
- Cache Status and Reasons,本次核对页面标注更新 2026-09-15。
- Response headers。
- vercel logs。
- vercel metrics。
- Runtime Logs。