Webhook2026年8月6日閱讀時間 8 分鐘

Slack 給你 3 秒,沒趕上,所有人都看得到

Slack 要求三秒內回 2xx,一小時內有 95% 的傳遞失敗,就會停用你的事件訂閱。這是使用者會親眼看到的 Webhook 失敗。

3 秒,接著是使用者看得到的三次重試第一次送出0 秒第 1 次重試~0 秒第 2 次重試1 分鐘第 3 次重試5 分鐘處理程式不冪等時,頻道裡看到的樣子慢的處理程式4 秒發出訊息又發一次再發一次
三秒,然後三次重試。如果處理程式不是冪等的,每次重試都會在大家正在看的頻道裡再發一次訊息。

你的機器人在開發環境運作得很完美,到了正式環境,卻只是有時候會回。使用者回報它「通常」會回覆,或是同一則訊息回了三次,或是整個下午都沒聲音,然後自己又恢復了。

這三種都是同一個 bug。Slack 給你三秒,你的處理程式花了四秒,之後發生的一切,都是 Slack 的重試機制碰上一個沒預料到重試的處理程式。

重點摘要

  • 「你的 App 應該在三秒內,以 HTTP 2xx 回應事件請求」
  • Slack 會重試三次:幾乎立刻一次,1 分鐘後一次,再來是 5 分鐘後。
  • 不冪等的處理程式,會把這些重試變成大家都看得到的重複訊息。
  • 對你確定處理不了的事件,在非 200 的回應加上 x-slack-no-retry: 1,就能停止重試。
  • 60 分鐘內有超過 95% 的傳遞失敗,Slack 就會停用你的事件訂閱,並寄信給 App 的擁有者。

三秒做不了多少事

看看一個常見的處理程式,從收到 message.channels 到準備好回覆之間做了什麼。它驗證簽章,查出這是哪個工作區,從資料庫讀一些狀態。然後,在 2026 年,機器人只要做點有意思的事,就會呼叫模型。

前三件事,順利的話大概 200 毫秒。第四件要 30 秒。

這和付款 Webhook 遇到的時間尺度落差是同一回事,差別在於這裡的回應時限是公開寫明的三秒(每個發送端的回應時限,都整理在我們的 Webhook 服務商參考資料),而且後果看得見,不是無聲無息。沒有人會盯著一個 payment_intent.succeeded 失敗,但每個人都會看到機器人沒回應。

適用於所有發送端、所有你可能使用的託管平台的通用解法,寫在你的 Webhook 處理程式呼叫 LLM;逾時檢查工具則會拿 Slack 的三秒,替你的處理程式計時。Slack 另外還有一個專屬的逃生口,讓它比多數服務商好處理得多。

Slack 給你、Stripe 沒給的技巧

Slack 把確認收到事件和回應使用者分開。你先立刻回 200,之後再用 chat.postMessage,或打互動 payload 帶的 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 收到兩次。

沒人在用的標頭

有些事件永遠不會成功:工作區已經移除你的 App、頻道已經封存、payload 參照的物件已經被你刪掉。重試這些事件會浪費三次傳遞,更重要的是,會把你的失敗率推向讓事件訂閱被關閉的門檻。

Slack 讓你在每次回應時選擇不要重試:「在你非 200 OK 的回應中,提供這個 HTTP 標頭和值:x-slack-no-retry: 1」。

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

只用在不可重試的錯誤上。資料庫逾時不屬於這種,你會需要那次重試。

失敗門檻

把一個變慢的下午變成服務中斷的,就是這部分。

「如果你的 App 在 60 分鐘內,有超過 95% 的傳遞嘗試落入這些失敗情況的任意組合,你的 App 的事件訂閱會被暫時停用。」

發生這種情況時,Slack 會寄信給 App 的建立者和擁有者。這只有在那個信箱的主人還在公司、而且還在看信時才有用。這和 Shopify 的緊急開發者信箱是同一個陷阱,詳見 Shopify Webhook 訂閱被刪除了:你唯一收到的警告,被送到一個設定時選好、之後再也沒人檢查的信箱。

值得記住的是:一小時內 95%,不是你會注意到的緩慢惡化。一次弄壞簽章驗證的部署,會立刻達到 100%,一小時後事件訂閱就關了。機器人不會報錯,它只是再也收不到任何東西,從內部看,就和一個安靜的頻道一模一樣。

機器人時好時壞,怎麼除錯

依序檢查這些。

回應時間,而不是錯誤率。一個 2.8 秒的處理程式,能通過你寫的每個測試,但只要資料庫稍微忙一點就會失敗。量 p99,不要量平均值,因為讓 Slack 逾時的就是 p99。

冷啟動,如果你用的是無伺服器。三秒的回應時限被冷啟動吃掉兩秒,其他所有事情只剩一秒。在 Vercel 上接收 Webhook,不讓事件遺失說明了怎麼讓路由的 import 圖保持精簡,在這裡,這比任何其他做法都更能把時間要回來。

那些重複是不是 Slack 的重試。第一則訊息之後六分鐘出現的重複訊息,是 Slack 的第三次重試,不是你發訊息邏輯的 bug。從時間點就分辨得出來。

事件訂閱是否仍然啟用。如果一切突然同時安靜下來,log 裡也沒有任何變化,在除錯其他東西之前,先去看 App 的 Event Subscriptions(事件訂閱)頁面。

AnyHook 的角色

三秒是傳輸層的限制,而 Webhook 中繼服務(webhook relay)取代的正是傳輸這一段。

把 Slack 的 Request URL(請求網址)指向 in.anyhook.net/you/slack。AnyHook 在邊緣節點驗證並儲存事件,大約一秒就回 200,不管你的處理程式是熱的、冷的,還是正在著火。滿足 Slack 三秒計時的,是一個永遠只做兩件事的邊緣 worker,所以重試序列幾乎不會開始,95% 的門檻也從來不會接近。

  • 依方案不同,在運算額度內,你的處理程式有 60 到 300 秒可用,而不是三秒。
  • 每個事件都在傳遞前儲存,所以部署期間的事件只需要一次重播,而不是一堆使用者的疑問。
  • 傳遞到你端點的重試採用指數退避,持續時間遠比 Slack 六分鐘的序列長。
  • 連續失敗 1、5、20 次時發出失敗警示,遠在任何數字到達 Slack 會採取行動的門檻之前。

AnyHook 會在邊緣節點回應 url_verification 握手,如果你提供了簽章密鑰,會先檢查 Slack 的簽章,而且不會把任何東西轉送給你的處理程式。

結論

Slack 的三秒,是所有主要發送端裡最緊的回應時限,也是少數錯過時會被產品使用者看見的。永遠先確認收到,再回覆。在做任何會讓人看到兩次的事情之前,先用 event_id 去重。確定事件已經沒救時,送出 x-slack-no-retry: 1。另外,去確認 App 的擁有者登記的是哪個信箱,因為事件訂閱被關掉時,通知就是寄到那裡。

所有文章2026年8月6日 · 8 分鐘

別再漏掉 Webhook。

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