Webhook2026年8月6日8 分钟阅读

Slack 给你 3 秒,超时了所有人都看得见

Slack 要求三秒内返回 2xx,一小时内 95% 的投递失败就会禁用你的事件订阅。这种 Webhook 故障,用户会亲眼看着它发生。

3 秒,然后是用户看得见的三次重试首次发送0秒第 1 次重试~0秒第 2 次重试1分钟第 3 次重试5分钟处理程序不幂等时,频道里会看到什么慢处理程序4秒发出消息又发一次再发一次
三秒,然后是三次重试。如果处理程序不幂等,每次重试都会在有人正在看的频道里再发一条消息。

你的机器人在开发环境里一切正常。到了生产环境,它有时回复,有时不回复。用户反馈说它“通常”会回复,或者对一条消息回复了三次,或者整个下午都没动静,然后自己又好了。

这三种其实是同一个 bug。Slack 给了你三秒,你的处理程序用了四秒,之后发生的一切,都是 Slack 的重试策略撞上了一个没考虑到重试的处理程序。

TL;DR

  • “你的应用应在三秒内以 HTTP 2xx 响应事件请求”
  • Slack 会重试三次:几乎立即一次,1 分钟后一次,5 分钟后再一次
  • 不幂等的处理程序会把这些重试变成大家都看得见的重复消息
  • 对于你明知处理不了的事件,在非 200 响应里带上 x-slack-no-retry: 1,就能停止重试
  • 60 分钟内超过 95% 的投递失败,Slack 就会禁用你的事件订阅,并发邮件通知应用的所有者

三秒干不了多少事

看看一个典型的处理程序,从收到 message.channels 到准备好回复之间都做了什么。它验证签名。它查出这是哪个工作区。它从数据库里读一些状态。然后,如果这个机器人在 2026 年还算在做点有意思的事,它会调用一个模型。

前三步顺利的话大概 200 毫秒。第四步要 30 秒。

这和支付 Webhook 遇到的时间尺度不匹配是同一个问题,只不过这里的响应时限是公开写明的三秒(各个发送方的响应时限都在我们的 Webhook 平台参考里),而且后果是看得见的,而不是静悄悄的。没人会盯着一个 payment_intent.succeeded 失败。但机器人不回话,所有人都看着。

适用于所有发送方、所有托管平台的通用修复思路,写在你的 Webhook 处理程序调用了大模型(LLM)里,而超时检测工具可以拿 Slack 的三秒给你的处理程序计时。Slack 还有一个专门的退路,让它比大多数平台好对付得多。

Slack 给你、Stripe 不给你的那个技巧

Slack 把应答事件和回复用户分开了。你立即返回 200,之后再用 chat.postMessage 发出真正的回复,或者请求交互请求体里带的 response_url。

export async function POST(req: Request) {
  const raw = await req.text();
  if (!verifySlackSignature(raw, req.headers)) {
    return new Response("bad signature", { status: 401 });
  }

  const body = JSON.parse(raw);

  // URL verification handshake, only during setup
  if (body.type === "url_verification") {
    return Response.json({ challenge: body.challenge });
  }

  await queue.publish(body);                        // durable, ~10ms
  return new Response(null, { status: 200 });       // well inside 3s
}

然后 worker 需要多久就花多久,有话要说时再调用 chat.postMessage。在用户看来,这就是机器人在思考,机器人思考一下很正常。在 Slack 看来,这个事件在 40 毫秒内就得到了应答。

如果工作要花一两秒以上,就先立即发一条占位消息,再更新它。一条先写着“正在处理”、随后自己更新内容的消息,体验远好于三秒的沉默之后来一个超时。

重试实际上会对你做什么

Slack 的重试计划短而明确:“第一次重试几乎会立即发送。第二次重试会在 1 分钟后进行。第三次也是最后一次重试,会在 5 分钟后发送。”

现在设想一个先干活、最后才返回 200 的处理程序,它要花四秒。Slack 在第三秒超时。你的处理程序在第四秒完成,发出一条消息。Slack 几乎立即重试,你的处理程序再跑一遍,再发一条。一分钟后,第三条消息。五分钟后,第四条。

3 秒,然后是用户看得见的三次重试首次发送0秒第 1 次重试~0秒第 2 次重试1分钟第 3 次重试5分钟处理程序不幂等时,频道里会看到什么慢处理程序4秒发出消息又发一次再发一次
重试序列很短,而且是公开的,频道里的人全都看得见。

用户问了一个问题,六分钟内收到四个回答,这看起来不像一个慢吞吞的机器人,更像一个坏掉的机器人。

修复方法和其他地方一样,用唯一约束,以 Slack 的 event_id 为键:

const { rowCount } = await db.query(
  `INSERT INTO processed_events (event_id, provider)
   VALUES ($1, 'slack') ON CONFLICT (event_id) DO NOTHING`,
  [body.event_id],
);
if (rowCount === 0) return new Response(null, { status: 200 });

注意重复路径上返回的是 200。在这里返回错误,等于让 Slack 重试你刚刚成功忽略掉的东西。两次重试在同一毫秒落到两个实例上的并发情况,详见同一个 Webhook 收到了两次。

没人用的那个响应头

有些事件永远不会成功。工作区卸载了你的应用,频道被归档了,请求体引用了一个你已经删掉的对象。重试这些事件会浪费三次投递,更重要的是,会把你的失败率推向让订阅被关掉的那个阈值。

Slack 允许你按单个响应选择不重试:“在你的非 200 OK 响应中,提供这个 HTTP 头和值:x-slack-no-retry: 1”。

if (!workspace) {
  return new Response("unknown workspace", {
    status: 410,
    headers: { "x-slack-no-retry": "1" },
  });
}

只在不可重试的情况下用它。数据库超时并非不可重试,你需要那次重试。

失败阈值

就是这一部分,会把一个慢吞吞的下午变成一次服务中断。

“如果你的应用在 60 分钟内超过 95% 的投递尝试出现上述失败情况的任意组合,你的应用的事件订阅将被暂时禁用。”

发生这种情况时,Slack 会给应用的创建者和所有者发邮件。只有当这个邮箱的主人还在公司、而且还在看这个邮箱时,这封邮件才有用。这和 Shopify 的紧急开发者邮箱是同一个坑,见 Shopify Webhook 订阅被删除了:你收到的唯一一次警告,会被发到一个设置时填写、之后再也没人检查过的地址。

值得记住:一小时内 95%,不是那种你会注意到的缓慢恶化。一次搞坏签名验证的部署会立刻让失败率达到 100%,一小时后订阅就被关掉了。机器人不会报错。它只是什么都收不到了,从内部看,这和一个安静的频道完全一样。

排查一个时而回复、时而不回复的机器人

按顺序检查下面几项。

看响应时间,而不是错误率。一个耗时 2.8 秒的处理程序能通过你写的每个测试,但只要数据库稍微忙一点就会失败。衡量 p99 而不是平均值,因为 Slack 超时看的就是 p99。

如果你用的是 Serverless,看冷启动。三秒的响应时限里,冷启动占掉两秒,留给其他所有事情的只有一秒。在 Vercel 上接收 Webhook,不让事件丢失讲了如何让路由的导入依赖图保持精简,在这方面,它能帮你省回的时间比其他任何手段都多。

看这些重复是不是 Slack 的重试。第一条消息六分钟后出现的重复消息,是 Slack 的第三次重试,不是你发消息逻辑里的 bug。看时间就能分辨。

看订阅是否仍然启用。如果一切突然都安静下来,而你的日志没有任何变化,先去看应用的 Event Subscriptions(事件订阅)页面,再排查别的。

AnyHook 能做什么

三秒是一个传输层面的约束,而传输正是转发服务替你接管的部分。

把 Slack 的 Request URL(请求 URL)指向 in.anyhook.net/you/slack。AnyHook 在边缘节点验证并持久化,约 1 秒返回 200,不管你的处理程序是热的、冷的,还是已经烧起来了。满足 Slack 三秒时限的,是边缘节点上一个只做两件事的 Worker,所以重试序列几乎不会启动,95% 的阈值也永远不会被逼近。

  • 你的处理程序有 60 到 300 秒,而不是三秒,具体取决于套餐,并以计算额度为限
  • 每个事件在投递之前就已持久化,所以部署期间的空窗只需要一次重放,而不是用户来问一堆问题
  • 向你的端点投递时按指数退避重试,持续时间远长于 Slack 六分钟的重试序列
  • 连续失败 1 次、5 次和 20 次时发送失败告警,远在任何指标达到 Slack 会采取行动的阈值之前

AnyHook 在边缘节点应答 url_verification 握手,如果你给了它签名密钥,会先验证 Slack 的签名,并且不会向你的处理程序转发任何内容。

总结

Slack 的三秒是所有主流发送方中最紧的响应时限,也是少数几个超时会被你的产品用户直接看到的场景之一。永远先应答,再回复。在做任何人会看到两次的事情之前,先按 event_id 去重。当你知道这个事件已经没救了,就发送 x-slack-no-retry: 1。再去看看应用上登记的所有者是哪个邮箱,因为你的订阅被关掉时,通知就发到那里。

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

别再丢 Webhook 了。

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