架构2026年8月3日8 分钟阅读

Webhook 与轮询:按故障模式选择,而不是跟风

轮询浪费请求,但出问题时能平稳降级。Webhook 高效,却会无声地失败。另附大多数像样的集成最终都会搭建的混合方案:Webhook 加对账轮询,最多晚 15 分钟。

轮询 · 缺口会自己补上你的循环宕机 3 小时下一次轮询since=cursor已追平恢复不需要任何你没写过的代码Webhook · 缺口看不见平台500s 3小时重试,然后放弃什么都没有没有报错,没有游标任何地方都没有信号没有数据,看起来和没有活动一模一样
要比的不是每小时多少次请求。要比的是每种模式出故障时会留下什么。

常见的说法是:轮询是入门的笨办法,Webhook 才是进阶之后该用的。这个说法很工整,却常常出人意料地把团队引向错误的架构。

有用的问题不是哪个更高效。Webhook 更高效,而且差距很大。有用的问题是每一种出故障时会怎样,因为凌晨 3 点你要面对的正是这个特性,而这两者几乎完全相反。

TL;DR

  • 轮询的失败动静很大,而且可恢复:你会注意到,下一次轮询也会把数据补上
  • Webhook 的失败静默,而且永久:没有任何报错,事件就这么没了
  • Webhook 逼你自己搭建轮询免费附带的基础设施:重试、去重、顺序和签名验证
  • 大多数生产环境的集成最后都是混合方案:Webhook 负责时效,对账轮询负责正确性
  • 如果只能做一种,而且正确性比时效更重要,那就轮询

通常给出的对比

轮询Webhook
时延取决于轮询间隔,平均是间隔的一半接近实时
浪费的请求大部分没有
谁承担成本你发送方
需要公网端点否是
顺序由你控制不保证
搭建一个 HTTP 调用加一个循环端点、TLS、签名验证、去重、重试

看这张表,Webhook 在大家通常最在意的那几行上都赢了。所以才有那么多团队换过去、上线,然后在两个月里悄悄丢掉事件。

真正起决定作用的对比

换个问题问:它出故障的时候,我怎么知道?

轮询的降级方式会自己暴露出来。你的服务器宕机了,循环就不跑。服务器恢复后,下一次轮询会问“从我上一个游标之后有什么变化”,然后拿到全部。恢复是自动的,不需要任何你原本没写过的代码。如果 API 挂了,你的轮询会收到 500,你马上就能在自己的错误率里看到。拉取模式意味着你始终知道自己最后一个确认无误的位置,光这一个特性就价值巨大。

Webhook 的失败方式在你这一侧是看不见的。一次糟糕的部署让你的端点连续六个小时返回 500。平台重试、放弃,然后就不管了。你的日志里看不出任何异常,因为你没法记录一个从没收到过的请求。你的系统里没有游标,没有缺口检测,也没有任何地方显示“我落后了 400 个事件”这样的数字。没有数据和没有活动看起来完全一样,在一个安静的周日,你根本分不出来。

轮询 · 缺口会自己补上你的循环宕机 3 小时下一次轮询since=cursor已追平恢复不需要任何你没写过的代码Webhook · 缺口看不见平台500s 3小时重试,然后放弃什么都没有没有报错,没有游标任何地方都没有信号没有数据,看起来和没有活动一模一样

该由这种不对称来做决定。轮询最坏的情况是数据过时,但你会追上。Webhook 最坏的情况是数据缺失,而你永远不会知道。

连带的成本也由此而来。用了 Webhook,重试逻辑就归你管了,因为平台的重试窗口是有限的,再大方的平台三天后也会放弃。幂等性归你管,因为至少一次(at-least-once)投递让重复成了必然。顺序保护归你管,因为重试会让事件 #2 赶在事件 #1 之前到达。签名验证归你管,因为你的端点是公开的,谁都能向它发 POST。你还要维护一个始终在线的公网 HTTPS 端点,这和“我们的应用一般都在线”是两种不同的可用性要求。

单独看,这些都不难。合在一起,它们就是“直接用 Webhook 不就行了”最后变成两周工程的原因,而且在你以为完工的时候,它其实还没完。

轮询真正适合的场景

遇到下面这些情况,别被效率上的理由劝退。

最明显的一种是你并不需要低时延。每晚一次的会计同步、库存对账、CRM 数据补全任务:如果 5 分钟的延迟没人会注意到,你就是在为一个对谁都没有价值的特性付出实实在在的复杂度。

然后是根本没有地方接收。桌面应用、VPN 后面的内部工具、跑在某个人笔记本上的任务。用内网穿透把 Webhook 引到 localhost 是一种开发手段,而不是生产架构;这条边界在哪里,我们在为什么 ngrok 不够用里写过。

团队最常判断错的一种,是正确性比时效更重要、而截止日期只剩一周的时候。一个你能想清楚的基于游标的轮询,胜过一条只做了一半的 Webhook 管道。

量小的时候也是一样。每 5 分钟轮询一次,一天是 288 个请求,任何限流都察觉不到,运维上的简单还是白送的。平台自己的 Webhook 不靠谱时也是如此:如果它的投递不可靠、事件日志又单薄,轮询它的 API 比信任它的发送端更可靠。

什么时候 Webhook 值得这份复杂度

有人在等的时候,Webhook 的复杂度就值了。支付确认、部署状态、一条聊天消息。这时时延能否压到一秒以下很关键,而要靠轮询快到看起来像实时,就得不停地猛刷 API。

事件稀少又不可预测时,Webhook 也会赢。为一天只发生两次的事情去轮询,意味着大约 99.9% 的请求什么都拿不到,而这正是 Webhook 设计要解决的情况。另一个极端是,量大到轮询根本不可行:一万家店铺、每家都要求 30 秒的新鲜度,一天就是 28,800,000 个请求,远在达到这个数之前你就会撞上限流。

最后一种是长时间运行的任务。视频转码、模型推理、批量导出。平台知道它什么时候完成,你只能猜,所以 AI 和媒体类 API 都在大力推 Webhook。见面向 AI 智能体(AI Agent)的 Webhook。

混合方案:反正你最后也会做成这样

几乎每个成熟的集成最后都会走到这一步,主动走过来,比出了事故之后再走过来便宜。Webhook 负责时效;定期的对账轮询负责正确性。

// Fast path: webhook, near-real-time
export async function POST(req: Request) {
  const event = await verify(req);
  await upsertEvent(event);           // idempotent by provider event id
  return new Response(null, { status: 200 });
}

// Safety net: every 15 minutes, ask what we should have
async function reconcile() {
  const now = new Date();             // before listing, so nothing updated mid-sweep is skipped
  const since = await getLastReconciledAt();
  for await (const obj of provider.list({ updated_since: since })) {
    await upsertEvent(obj);           // same idempotent path, no-op if webhook won
  }
  await setLastReconciledAt(now);
}

对账任务的代价是一个定时函数,复用的是你已经写好的代码。换来的是:Webhook 最糟糕的特性,也就是无声的永久丢失,变成了“最多晚 15 分钟”。这是完全不同的风险。最坏的情况甚至不是漏掉一笔付款(总会有人发现),而是漏掉一个 access.revoked,这个没人会发现。

让它成立的前提是:两条路径都经过同一个幂等的写入函数。如果 Webhook 已经处理过,对账就什么也不做。如果没处理,对账就把它补上。只有按平台的事件 ID 去重,而不是按你自己的接收时间去重,这一点才成立。具体模式见同一个 Webhook 收到了两次。

然后给重叠的部分加上度量。统计对账有多少次找到了 Webhook 漏掉的东西。这个数字就是你的 Webhook 管道真实的丢失率,也是你唯一能得到的诚实度量。如果它连续一个月为零,你的 Webhook 是健康的。如果它突然飙升,就说明发生过一次你原本永远发现不了的宕机。

被遗忘的中间地带

轮询不一定是笨办法。

基于游标的轮询,也就是用 ?since=<cursor>,而不是“全量拉取再做比对”,成本低得多,而且从不漏掉任何一条,因为只有处理成功后游标才会前进。

条件请求,也就是带 ETag 的 If-None-Match 或 If-Modified-Since,会返回一个没有响应体的 304。很多平台不把这类请求计入你的限流额度。你可以很激进地轮询,几乎不花什么成本。

长轮询能给你接近 Webhook 的时延,同时保留拉取模式的恢复能力。它比较少见,但只要平台提供,往往能同时拿到两边的好处。

如果你因为按固定间隔全量拉取的成本而否定了轮询,那你拿来比较的可能是轮询最差的那个版本。

AnyHook 能做什么

Webhook 之所以会无声地失败,是因为没有人记录本该到达的是什么。AnyHook 就是这份记录。

平台向 in.anyhook.net/you/app 发送 POST。每个事件都会在尝试投递之前持久化,所以:

  • 缺口变得可见。投递失败会被计数、展示出来,并在连续失败 1 次、5 次和 20 次时告警,“无声”二字就不再成立
  • 恢复靠的是重放,而不是对账任务。选中你的服务器宕机的那段时间,从日志里重放。重放失败的事件不计入你的月度配额,不过有每日上限,并且会消耗你的计算额度
  • 基础设施的代价由我们一次付清:指数退避重试、持久化日志,以及统一的 AnyHook-Signature 请求头,而不必为每个平台各写一套验证方案

这并不意味着就不需要对账轮询了。如果平台根本没发出这个事件,任何转发服务都变不出来。但它把故障模式从“无声且永久”缩小成了“有日志、有告警、可重放”,而这正是混合方案为你换来的大部分东西。

想在注册任何东西之前先看看效果,一条命令就能返回一个可用的接收 URL,不需要账号:

curl -X POST https://anyhook.net/api/v1/quickstart

响应里有一个 inbound_url,用来填到平台那里;一个 api_key,用来通过 API 读取事件日志;还有一个 claim_url。这个端点是临时的:除非你打开认领 URL 并登录,否则 7 天后它和它的事件都会被删除;认领之后 URL 保持不变。

总结

按故障模式来选。轮询慢,但能自愈;Webhook 快,但会悄悄丢东西。如果时延既不影响人,也不影响限流,那轮询就不是新手选项,而是最坏情况更好的那个。如果你确实在生产环境里跑 Webhook,就在需要之前加上对账轮询,因为它产出的指标,是你知道自己实际丢了多少事件的唯一办法。

全部文章2026年8月3日 · 8 分钟

别再丢 Webhook 了。

只改一个 URL。获得重试、事件日志和一键重放。也可以先亲眼看看:注册后跑一次 30 秒的故障演练,看一个事件如何挺过你的服务器宕机。不需要服务器。