静态站表单 5 条免费链路实测 13 天:3 条全死,接口还在返回 ok:true

2026-11-02 · 实测数据 · 阅读约 9 分钟

一个静态站最脆的地方不是页面,是表单。页面挂了你能看见,表单挂了你看不见——访客点了提交,转圈两秒,出现「提交成功」,然后那条询盘进了黑洞。

我给自己的站做了 5 条并行链路(业内叫冗余),每天开工第一条动作就是真发一条探针,逐腿读返回。连续 13 天,得到一张很难看的表:5 条腿里 3 条 13/13 天全失败,而接口顶层从第一天到最后一天都返回 ok:true。这篇文章把原始读数、错误码成因、和一套能真正判死活的验收清单全部摊开。

一、被测的 5 条腿长什么样

表单是 Cloudflare Pages Functions 写的一个 /api/lead,收到 POST 后往 5 个方向各发一份,最后聚合成一个 JSON 返回:

腿实现设计意图成本
腿1webhook.site 临时桶 ×5落库(能回读原始 JSON)免费,50 条 / 7 天
腿2Amazon SES发到自己的邮箱按封计费,几分钱
腿3ntfy.sh手机推送免费,有日额度
腿4FormSubmit第三方转发到邮箱免费,有日额度
腿5自建 SMTP 中继备用发信自有服务器

这个结构看起来很稳:任何一条挂了还有其他四条。实际结果正好相反——它把「全死」伪装成了「全活」。

二、13 天逐日读数(原始值,未加工)

下表是每天 06:30 班第一条动作的实测返回,sent 为成功,其余为原始错误码:

日期腿1 桶腿2 SES腿3 ntfy腿4 FormSubmit腿5 SMTP
10-20sent×5sent522429401
10-21sent×5sent429429401
10-22sent×5sent429429401
10-23sent×5sent522429401
10-24sent×5sent429429401
10-25sent×5sent429429(当日实收 1 封)401
10-26sent×5sent429429401
10-27sent×5sent429429401
10-28sent×5sent522429401
10-29sent×5sent522429401
10-30sent×5sent429521401
10-31sent×5sent522429401
11-01sent×5sent429429401
11-02sent×5sent522429401

统计:腿1 13/13 成功、腿2 13/13 成功、腿3 0/13、腿4 0/13、腿5 0/13。所谓「5 条冗余」,有效冗余是 2 条。

注意 10-25 那格:FormSubmit 返回 429,但当天邮箱确实收到了 1 封转发。这说明429 不等于链路死,它只是「这一次被限流」。单次探测只能判「此刻」,判不了「当天」。

三、三条腿为什么全死:错误码各自的含义

1) ntfy.sh:429 和 522 轮流出现

429 是免费日额度用尽,522 是 Cloudflare 侧连不上源站(ntfy 自己的边缘超时)。这两个码在 13 天里交替出现,说明这条腿不是「被墙」也不是「配置错」,而是免费额度 + 服务端不稳定。同一个 URL、同一个请求体,隔天结果不同——这种腿你永远无法用「上次是好的」来推断今天。

2) FormSubmit:429 是日额度,521 是源站挂了

FormSubmit 免费层按域名维度限流,共享同一出口 IP 的请求很容易互相踩。521 出现在 10-30,是 Cloudflare 标记「源站拒绝连接」,与你的表单无关,等它自己恢复。

3) 自建 SMTP:401 不是密码错,是 nginx 挡在前面

这条最有教育意义。返回体是一整页 HTML:<title>401 Authorization Required</title> ... nginx/1.27.1。也就是说请求在 HTTP 层就被 nginx 的 basic auth 拦住了,根本没走到应用层的账号密码校验。看到 401 就去改 SMTP 密码,是典型的层级误判——先看是谁返回的(Server 头),再决定改哪一层。

四、最贵的一条教训:ok:true 只证明函数跑完了

这是整件事的核心。我的接口返回长这样:

{
  "ok": true,
  "results": {
    "sink": "sent | sent | sent | sent | sent",
    "ses": "sent",
    "ntfy": "failed:522",
    "formsubmit": "failed:429",
    "mail": "failed:401"
  },
  "intent": "normal"
}

ok:true 的含义是「函数执行完毕、没有抛异常」。它不代表任何一条腿送达。如果你只监控 ok,你的监控会连续 13 天报「全部正常」,同时三条腿一次都没成功过。

判活的最小单位不是「请求」,是「回执」。请求返回 200 只说明对方接收了这个 HTTP 包;只有对方能在自己的存储里被读回来,才算送达。

我自己的判据后来改成三条,缺一条就不算健康:

  1. 逐腿读:解析 results 的每个字段,不看 ok;
  2. 回读验证:往桶里发一条带唯一标记的探针,再用 GET 把桶内容读回来,确认标记在里面;
  3. 至少一条腿真的进人工视野:邮箱里能看到、或手机能收到推送。落库成功但没人看,等于没收到。

五、webhook.site 免费桶的三个硬限(9 次爆桶换来的)

腿1 虽然 13/13 成功,但它有另一类死法——不报错地死。免费桶的规则:

限制实测值失效表现
条数上限50 条 / 桶满了之后 total 冻在 50,新请求仍返回 200,但不再入库
有效期7 天过期后桶 404,之前的数据一起消失
建桶限流约 52 秒 / 次连建两个报 try again in ~52s

第 2 条最阴:过期前没有任何提醒。所以我的做法是把「再造新桶」写进每 2~3 天的固定动作,并且新桶建好立刻发一条探针确认能收——建桶成功不等于能收数据,这两件事要分别验。

条数上限这一条我踩了 9 次。前 8 次都是「桶满了 → 换桶 → 又满了」,直到第 8 次我才意识到问题不在桶,在流量估算。

六、用桶的容量反算真实流量:我的估算错了 70 倍

遥测(页面访问计数)也写在这个桶里,为了不爆桶我做了抽样。抽样率的调整史,本身就是一部误判史:

时间抽样率依据结果
10-301/6凭感觉「日访问很少」当天爆桶
10-30 晚1/10同上,保守一档24 小时内积 48/50
10-311/60按 48 条 × 10 反算 ≈ 480 次/天勉强稳住
11-011/150留 3 倍余量(480 ÷ 150 ≈ 3 条/天)24 小时仅 1 条

关键推理:1/10 抽样下 24 小时积了 48 条,反推真实命中 = 480 次/天。而我此前一直按「一天七八次访问」在做容量假设——差了 70 倍。这意味着在那之前所有基于「流量很小、不用管容量」的判断全部不可信。

方法上的结论:抽样率不能拍脑袋,必须用「容量 ÷ 实测日均」反算,并且留 3 倍余量。任何「我觉得访问量不大」的自我判断,都要有一次真实计数来背书。

七、静态站表单的最小可靠架构

踩完这些坑,如果重做一次,我会这样搭(按重要性排序,前三项占 90% 的可靠性):

  1. 自己可控的落库(必须有):Cloudflare KV / D1 / 或任何你能直接查询的存储。这是唯一不依赖第三方额度的环节,也是唯一能事后审计的环节。
  2. 一条事务性邮件(必须有):SES / 或你自己的 SMTP。要能查发送回执,不能只看接口返回。
  3. 一个「人一定能看到」的提醒(必须有):推到手机或微信。邮箱容易淹没在通知里。
  4. 第三方免费转发(可选,作冷备):FormSubmit 这类可以留着,但别把它算进可靠性分子——它有日额度、会 521、会 429。
  5. webhook 临时桶(可选,作取证):适合当「原始请求体存档」,别当主存储。50 条 / 7 天不够任何真实业务。

反面模式很明确:把 5 条免费腿当冗余。它们的失效模式高度相关——都是免费额度、都是同一类限流、都是没有 SLA 的公益服务。相关性高的冗余,等于没有冗余。真正的冗余应该是「一条付费 + 一条自建」,而不是「五条免费」。

八、可直接抄走的验收清单

#检查项通过判据
1逐腿状态能打印每一条腿的独立结果,不聚合
2回读发唯一标记 → GET 读回 → 标记在里面
3人工可达邮箱/手机上真的看到那条探针
4存储余量当前条数 / 上限 < 50%,且知道过期日
5失败可解释每个错误码能说清是「限流 / 源站挂 / 鉴权 / 配置」哪一类

这五条做完,大概 20 分钟。它挡住的不是技术问题,是「以为自己在收询盘、其实一条都没收到」这种最贵的沉默失败。

看这套巡检工具箱(含 9 项验收脚本)

常见问题

接口返回 200 就代表表单提交成功了吗?

不代表。200 只说明服务端接收了请求。要确认「消息到了」,必须能在下游存储里读回这条数据,或在下游收件箱里看到它。

FormSubmit 一直 429 是封号了吗?

多数是日额度或共享出口 IP 的限流,隔天会恢复。我实测过「返回 429 但当天仍有一封实际送达」,所以单次 429 不能判定链路死亡,要按天统计成功率。

webhook.site 免费桶能当询盘数据库用吗?

不建议。50 条上限 + 7 天过期 + 建桶限流,任一条都会让你在某天静默丢数据。它适合临时调试和原始请求取证,不适合当业务存储。

为什么我的多通道冗余没起作用?

如果几条通道都是免费第三方服务,它们的失效原因高度相关(额度、限流、源站稳定性),会同时死。冗余要选失效原因不同的通道,比如一条自建 + 一条付费。

静态站没有后端,怎么低成本做到「一定收得到」?

最小组合是:Pages Functions 收请求 → 写入 KV/D1(自己可控)→ 调一条事务性邮件 API。第三方转发只当冷备,不进可靠性分子。成本大约是一天几分钱。