Webhook2026年10月14日閱讀時間 9 分鐘

你的 Webhook 處理程式呼叫 LLM,發送端先掛斷了。

Slack 等 3 秒,Shopify 等 5 秒,GitHub 等 10 秒。呼叫 LLM 更久,發送端會掛斷。先回應再處理,或交給中繼服務代收。

在處理程式裡 · 發送端等著你的模型Slack驗證 + LLM 呼叫8 秒Slack 在 3 秒 掛斷重試 → 執行兩次在中繼服務後面 · 發送端的計時到此為止SlackAnyHook先儲存,再回 200佇列重試處理程式LLM 呼叫,最多 60 秒發送端大約 1 秒就收到 200;你的模型拿到它需要的時間
模型呼叫寫在處理程式裡,發送端就得等它。放在中繼服務後面,發送端只等中繼服務。

Webhook 發送端等的是幾秒,不是幾分鐘:Slack 給處理程式 3 秒回應,Shopify 給 5 秒,GitHub 給 10 秒。呼叫 LLM 的處理程式通常需要更久,於是發送端記下一次失敗,接著重試或放棄;錯過的次數一多,Slack 會把你的事件訂閱關掉,Shopify 可能會刪除這個 Webhook。解法是請求一通過驗證就回 200,等回應送出後再呼叫模型:放進你自己執行的背景工作,或放在替你回應發送端的 Webhook 中繼服務(webhook relay)後面。

以下是這兩句話的完整版:每個發送端實際等多久、錯過時會發生什麼事、四種先回應再處理的簡短寫法(以及各自的問題),還有改由中繼服務代收事件之後有什麼不同。

重點摘要

  • 發送端的計時涵蓋整個處理程式:簽章驗證、資料庫讀取,還有模型呼叫
  • 錯過時限,發送端就會重試。已經做完工作的處理程式會再做一次:Slack 裡出現兩則回覆、同一筆訂單被標記兩次、同一封信寄兩次
  • 錯過的次數一多,Slack 會停用你的事件訂閱,Shopify 可能會刪除 Webhook。GitHub 則完全不重試
  • 先回應,再處理:Next.js 用 after(),Cloudflare 用 ctx.waitUntil()(最多 30 秒),Express 或 FastAPI 用背景工作
  • 背景工作要是中途當掉,以上做法都留不住事件。持久的版本需要儲存空間、一個 worker 和重試排程,也就是一個佇列
  • 中繼服務就是你不必自己寫的那個佇列:AnyHook 大約一秒就回應發送端,Free 方案在運算額度內給處理程式 60 秒,並且會重試

各發送端等多久

以下數字取自各服務商自己的文件,出處整理在我們的 Webhook 服務商參考資料。

發送端等你回 2xx 的時間傳遞失敗之後
Slack3 秒重試 3 次:立即、1 分鐘後、5 分鐘後。60 分鐘內超過 95% 的嘗試失敗,就會停用事件訂閱
LINE2 秒(之後才回應會記為 request_timeout)重新傳送預設關閉。伺服器持續失敗時,可能停止傳送 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 秒,而冷啟動可能在第一行程式碼執行之前,就用掉其中大半。

為什麼呼叫 LLM 會出問題

在 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 搭配一個 consumer 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));
});

問題在於:這種寫法只撐到行程(process)重啟為止。部署、當機,或呼叫途中因記憶體不足被強制終止,進行中的工作都會遺失。在無伺服器平台上,回應送出的那一刻,行程也可能被凍結。

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。發送端從此不再過問;背景工作要是失敗,沒有第二次機會,事後也沒有任何東西可以查。

持久的版本有固定的做法:回應之前先把事件寫下來,再讓 worker 處理,失敗時重試,並留下足夠的紀錄,看得出哪些失敗、可以重新傳送。這就是一個附帶死信清單和重播按鈕的佇列。做起來不難,但它又多了一個要維運、要監控、出事要值班處理的東西,而且不是你的機器人存在的目的。想自己掌握這一套,我們的 Vercel 指南示範了先寫入再回應的版本。

如果你不想自己維運佇列

在處理程式前面放一個中繼服務。你只要在發送端把 Webhook 網址換成你的 AnyHook 網址,其他什麼都不用改。

  • 發送端大約一秒就會收到 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 驗證,網址第一次就能儲存成功

傳遞是至少一次(at-least-once)。處理程式做完了工作,卻在逾時之後才回應,就會再收到同一個事件,所以要讓它冪等:X-AnyHook-Delivery 標頭(事件 ID,每次重試都相同)已經處理過的,就直接跳過。發送端自己的 ID,例如 Slack 的 event_id 或 GitHub 的 X-GitHub-Delivery,也會一併轉送。完整的寫法在重複事件與冪等性。

中繼服務做不到的,是在發送端需要真正的回應主體時替你回答:

  • Slack 斜線指令(slash command)和互動按鈕要求回應裡就有你的回覆,或要很快確認收到。先從你自己的端點回一個空的 200,再把真正的答案發到 Slack 傳來的 response_url,它在 30 分鐘內最多接受五則訊息
  • Discord 互動(interaction)必須在 3 秒內給出第一個回應。先回一個延遲回應(type 5),等模型的答案到了再編輯它;interaction token 在 15 分鐘內有效
  • Twilio 的語音和訊息 Webhook 要求回傳 TwiML。先立刻回 TwiML,慢的答案再透過 REST API 送出

只需要 2xx 的事件 Webhook,正是中繼服務的用途: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 redelivery(Webhook 重新傳送)預設關閉,可以在 channel 的 Messaging API 設定裡開啟。

前面多放一個佇列,機器人會回得比較慢嗎? 不會。中繼服務一儲存好事件就轉送,所以處理程式開始執行的時間點,和原本差不多。改變的是,模型在思考時,發送端不再一直佔著連線。只有傳遞失敗時,重試才會多花時間;而沒有重試的話,那個事件早就遺失了。

處理程式要跑超過 60 秒怎麼辦? Free 方案下,AnyHook 最多等處理程式 60 秒回應;Pro 方案等 120 秒,Scale 方案等 5 分鐘,以方案的運算額度為限。比這更久的工作,把任務排進佇列後就回 202,再在你這邊做完。中繼服務會把 202 視為已送達。

Discord 機器人和 Slack 斜線指令也適用嗎? 透過中繼服務不行。兩者都需要在 3 秒內拿到特定的回應主體,只有你自己的程式碼給得出來。照上面的說明,改用 Discord 的延遲回應和 Slack 的 response_url。只需要 2xx 的 Slack Events API,就可以放在中繼服務後面。

處理程式會收到同一個事件兩次嗎? 會,不管有沒有中繼服務都可能:發送端會重試,處理程式失敗時 AnyHook 也會重試。把每個處理程式都當成至少一次傳遞來寫,跳過已經處理過的 ID。

所有文章2026年10月14日 · 9 分鐘

別再漏掉 Webhook。

換一個網址,就有重試、事件紀錄和一鍵重播。或者先親眼看看:註冊後跑一次 30 秒的當機演練,看著一個事件在伺服器當機時保存下來。不需要伺服器。