Webhook2026年10月14日9 分钟阅读

你的 Webhook 处理程序调用大模型。发送方先挂断了。

Slack 只等 3 秒,Shopify 5 秒,GitHub 10 秒。调用大模型耗时更长,发送方会断开。先应答、后处理,或者让转发服务先把事件保存下来。

在处理程序里 · 发送方要等你的模型Slack验证 + 大模型调用8秒Slack 在 3秒 时挂断重试 → 执行两次放在转发服务后面 · 发送方的计时到此为止SlackAnyHook先保存,再返回 200队列重试处理程序大模型调用,最长 60秒发送方约 1 秒就能收到 200;你的模型拿到它需要的时间
在处理程序里调用模型,发送方就得等模型返回。放在转发服务后面,发送方只等转发服务。

Webhook 发送方只等几秒,不会等几分钟:Slack 给你的处理程序 3 秒应答,Shopify 给 5 秒,GitHub 给 10 秒,而调用大模型(LLM)的处理程序通常需要更久;于是发送方记下一次失败,重试或者放弃;失败得足够频繁,Slack 就会停用你的事件订阅,Shopify 则可能删除这个 Webhook。解决办法是请求验证通过后立即返回 200,等响应发出后再调用模型:要么放在你自己运行的后台任务里,要么放在一个替你应答发送方的转发服务后面。

下文是这两句话的详细版:每个发送方实际等多久,错过时限后它会怎么做,四种先应答、后处理的简短写法(以及各自的问题),还有改由转发服务保存事件时会有什么不同。

TL;DR

  • 发送方的计时覆盖整个处理程序:签名验证、数据库读取,还有模型调用
  • 错过时限,发送方就会重试。已经把活干完的处理程序会再干一遍:Slack 里两条回复,一个订单被打两次标签,两封邮件
  • 错过的次数够多,Slack 会停用你的事件订阅,Shopify 可能会删除这个 Webhook。GitHub 从不重试
  • 先应答,后处理:Next.js 用 after(),Cloudflare 用 ctx.waitUntil()(最多 30 秒),Express 或 FastAPI 用后台任务
  • 后台任务一旦挂掉,这几种写法都留不住事件。持久的版本需要存储、工作进程和重试计划,也就是一个队列
  • 转发服务就是那个不用你自己写的队列:AnyHook 约 1 秒应答发送方,在 Free 套餐下给你的处理程序 60 秒,以套餐的计算额度为限,并且会重试

每个发送方等多久

这些数字来自各平台自己的文档,连同出处汇总在我们的 Webhook 平台参考里。

发送方等你返回 2xx 的时间投递失败之后
Slack3 秒重试 3 次:立即、1 分钟后、5 分钟后。60 分钟内超过 95% 的尝试失败,事件订阅就会被停用
LINE2 秒(更晚的应答会被记为 request_timeout)重发默认关闭。如果你的服务器持续失败,LINE 可能暂停发送 Webhook
Shopify5 秒(建立连接限 1 秒)4 小时内重试 8 次。通过 Admin API 创建的订阅连续失败 8 次后会被删除
HubSpot5 秒,由整批(最多 100 条通知)共用24 小时内最多尝试 10 次
Paddle5 秒正式环境:约 2 天内最多尝试 60 次
GitHub10 秒无。3 天内可以手动重新传递
Twilio默认 15 秒,可配置默认重试 1 次,可配置为 0 到 5 次
Stripe未公开。它的文档要求在执行任何耗时逻辑之前返回 2xx真实模式下按指数退避重试,最长 3 天

你真正的时限,是你接收的发送方里最严格的那一个。Slack 上的机器人只有 3 秒,而一次冷启动可能在你的第一行代码运行之前就耗掉大半。

为什么调用大模型会出问题

在 2026 年写的处理程序里,模型调用几乎总是最慢的一步。一次简短的补全要几秒;带工具调用、长回答,或者 SDK 内部重试过的,要几十秒。再加上一次冷启动,第一个 token 还没到,发送方就已经挂断了。

接下来会发生什么取决于发送方,没有一种是好事:

  • Slack 会重试。第一次运行还在等模型,你的处理程序又收到了同一个事件。两次运行都会跑完,也都会发消息。用户看到机器人回答了两遍
  • Shopify 先重试,再删除。四个小时里连续失败八次,通过 Admin API 创建的订阅就没了。订单还在不断进来,你的应用却再也收不到通知
  • GitHub 什么也不做。这次投递被标记为失败,事情到此为止,除非有人在三天内手动重新传递
  • LINE 的重发是关闭的,除非你打开过,所以一次超时就是一条你的机器人永远没有回复的消息

处理程序并没有出错。它只是比一个它看不见的时钟慢了。

解决办法:先应答,后处理

验证请求,返回 200,然后再调用模型。每个平台都有办法在响应之后继续运行。每种办法也都有一个问题,写在代码下面。

Next.js (App Router)

// app/api/slack/events/route.ts
import { after } from "next/server";

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);
  if (body.type === "url_verification") {
    return Response.json({ challenge: body.challenge });
  }

  after(async () => {
    const answer = await askModel(body.event.text); // the slow part
    await postToSlack(body.event.channel, answer);
  });

  return new Response(null, { status: 200 }); // well inside 3 seconds
}

问题在于:after() 只能运行到路由的最长执行时间,不会更久。如果模型调用抛出异常,或者实例被回收,事件就没了。Slack 已经拿到了 200,不会再发一次,也没有任何地方记录过这个事件存在过。

Cloudflare Workers

export default {
  async fetch(req: Request, env: Env, ctx: ExecutionContext) {
    const raw = await req.text();
    if (!(await verify(raw, req.headers, env))) {
      return new Response("bad signature", { status: 401 });
    }
    ctx.waitUntil(handle(JSON.parse(raw), env)); // runs after the response
    return new Response(null, { status: 200 });
  },
};

问题在于:waitUntil 在响应之后最多只有 30 秒,由你传给它的所有任务共享,时间一到就会被取消。一次慢的模型调用放不进去。对于更长的任务,Cloudflare 自己给出的方案是 Queue 加一个消费者 Worker,这个方案是对的。我们的 Workers 指南讲了这个平台的其他坑。

Express 或任何常驻的 Node 服务器

app.post("/webhooks/slack", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifySlackSignature(req.body, req.headers)) return res.sendStatus(401);
  res.sendStatus(200); // answer first
  setImmediate(() => handle(JSON.parse(req.body)).catch(console.error));
});

问题在于:进程重启之前一切都好。一次部署、一次崩溃,或者调用中途因内存不足被杀掉,都会丢掉正在处理的一切。在 Serverless 平台上,响应一发出,进程还可能立刻被冻结。

Python (FastAPI)

from fastapi import BackgroundTasks, FastAPI, Request, Response

app = FastAPI()

@app.post("/webhooks/line")
async def line_webhook(request: Request, background: BackgroundTasks):
    raw = await request.body()
    if not verify_line_signature(raw, request.headers.get("x-line-signature")):
        return Response(status_code=401)
    background.add_task(handle_events, raw)  # runs after the response is sent
    return Response(status_code=200)

问题和 Express 一样:任务活在进程里。进程一重启,任务就没了,没有重试,也没有记录。

这四种写法的共同点

每一种都是在事件只存在于内存中的那一刻返回 200。发送方从此不再关心;如果你的后台任务失败了,没有第二次机会,事后也没有任何东西可查。

持久的版本是一个大家都熟悉的模式:应答之前先把事件写下来,然后让工作进程去处理,失败了就重试,并保留足够的记录,能看到哪些失败了,也能重放它们。这就是一个带死信列表和重放按钮的队列。它并不难写。可它又是一样要运行、要监控、出了事要被叫起来处理的东西,而且它并不是你的机器人要做的事。如果你想自己掌控这一层,我们的 Vercel 指南给出了先持久化、再应答的写法。

如果你不想自己运行队列

在处理程序前面放一个转发服务。你只需要在发送方那里把 Webhook URL 改成你的 AnyHook URL,别的都不用改。

  • 发送方约 1 秒就能收到 200。这个 200 发出之前,事件已经保存好了
  • 随后事件投递到你的处理程序,投递超时为:Free 套餐 60 秒,Pro 套餐 120 秒,Scale 套餐 5 分钟,以你套餐的计算额度为限。一次模型调用放得进去,所以处理程序可以直接在请求里把活干完,干完再应答
  • 如果你的处理程序失败或超时,AnyHook 会按自己的重试计划重试:5 分钟、30 分钟、3 小时、18 小时,之后每天一次;Free 套餐最多 3 次投递尝试,Pro 5 次,Scale 10 次
  • 每个事件都保存在可搜索的日志里(Free 套餐 3 天,付费套餐更久),修好 bug 之后,任何一个都可以重放
  • AnyHook 会替你应答 Slack 的 url_verification 验证请求,所以 URL 第一次就能保存成功

投递是至少一次(at-least-once)的。处理程序如果已经把活干完了,却在投递超时之后才应答,就会再次收到这个事件,所以要让它幂等:凡是 X-AnyHook-Delivery 请求头(即事件 ID,每次重试都相同)已经处理过的,一律跳过。发送方自己的 ID 也会一并转发,比如 Slack 的 event_id 或 GitHub 的 X-GitHub-Delivery。重复事件与幂等性一文给出了具体做法。

转发服务做不到的,是在发送方需要真正的响应体时替你回答:

  • Slack 的斜杠命令和交互按钮要求回复写在响应里,或者先快速应答。用你自己的端点返回一个空的 200 应答它们,再把真正的回答发到 Slack 给出的 response_url,它在 30 分钟内最多接收五条消息
  • Discord 的交互要求在 3 秒内给出初始响应。先回一个延迟响应(类型 5),等模型的回答到了再编辑它;交互令牌在 15 分钟内有效
  • Twilio 的语音和短信 Webhook 要求返回 TwiML。立即用 TwiML 应答,慢的回答通过 REST API 发送

事件类 Webhook(只需要一个 2xx 的那种)才是转发服务的用途:Slack 的 Events API、LINE、Shopify、GitHub、HubSpot、Stripe,以及其他大多数平台。

测一测你的处理程序要多久

你不用去猜你的处理程序落在这条线的哪一边。Webhook 超时检测工具会向你的端点发送一个测试请求,计时,并把结果和每个发送方的时限放在一起。它会测两次,所以安静一段时间后第一次请求偏慢,会如实显示为冷启动。

常见问题

为什么 Slack 说我的机器人超时了,或者为什么它回复了两次? Slack 等 2xx 只等 3 秒。如果你的处理程序在应答之前调用模型,耗时就会超过这个时间,Slack 记一次失败并再次发送事件,两次运行都会发出回复。先应答,再用 chat.postMessage 发送回复,并丢弃已经见过的事件。

在 Vercel 上,我能直接返回 200 然后继续处理吗? 可以,用 next/server 的 after()(或者 waitUntil),任务会一直运行到路由的最长执行时间为止。但它不持久:任务失败或者实例被回收,事件就丢了,发送方也不会重发。用来发通知没问题,用在用户会发现缺失的事情上就不对了。

LINE 等 Webhook 响应会等多久? 两秒。机器人服务器 2 秒内没有应答的 Webhook,LINE 会在错误统计里记为 request_timeout,并建议异步处理。Webhook 重发默认关闭;在频道的 Messaging API 设置里打开它。

在前面加一个队列,会让我的机器人回复变慢吗? 不会。转发服务在事件保存后立即转发,所以你的处理程序开始运行的时间,和原来差不多。变化在于,模型思考期间,发送方不用再一直占着连接。只有投递失败时,重试才会增加时间,而没有重试,那个事件本来就丢了。

如果我的处理程序要跑 60 秒以上怎么办? 在 Free 套餐下,AnyHook 会等你的处理程序应答 60 秒;Pro 套餐等 120 秒,Scale 套餐等 5 分钟,以你套餐的计算额度为限。比这更长的任务,在把任务放进队列后立即返回 202,然后在你这边完成。转发服务会把 202 计为已投递。

这对 Discord 机器人和 Slack 斜杠命令有用吗? 通过转发服务不行。两者都需要在 3 秒内返回特定的响应体,只有你自己的代码能给。按上文所说,使用 Discord 的延迟响应和 Slack 的 response_url。Slack 的 Events API 只需要一个 2xx,可以放在转发服务后面。

我的处理程序会收到同一个事件两次吗? 有可能,不管有没有转发服务:发送方会重试,你的处理程序失败时 AnyHook 也会重试。把每个处理程序都当作至少一次投递来处理,跳过已经处理过的 ID。

全部文章2026年10月14日 · 9 分钟

别再丢 Webhook 了。

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