AI InfrastructureMay 24, 20268 min read

為什麼你的 AI agent 需要 webhook relay

Stripe 給你 10 秒,OpenAI tool call 跑 45 秒——webhook 同步處理 AI 推論的時間錯位是 2026 年最常見的雙重扣款來源。這篇講為什麼會壞,以及正確的 edge ack + async deliver pattern。

某個 indie hacker 在 Stripe 接了 OpenAI Assistants 做付款後的訂閱分流——客戶付款成功,agent 跑工具呼叫去判斷該 provision 哪個 SKU、寫 DB、發 onboarding email。本機跑得好好的。上 production 第三天,他發現一個客戶被扣了三次 $49

打開 Stripe dashboard,payment_intent.succeeded 對著同一個 pi_xxx 被 deliver 了 3 次。每一次都 timeout,每一次 handler 都跑完了。也就是說:Stripe 認為 3 次都失敗,他的系統認為 3 次都成功。fulfillment 寫了 3 次,下游 provision 跑了 3 次,email 寄了 3 次。但客戶只付了一次——這不是 double-charge,是 double-fulfillment,影響更難察覺,因為 Stripe dashboard 看起來正常。

退款、道歉、加 idempotency key——一個禮拜後同樣的事情換成 invoice.paid 又發生一次。

為什麼 AI agent 跑 webhook 一定會壞

Webhook 是 HTTP 同步協議。Provider 發一個 POST 過來,等你回 2xx,超過 timeout 就視為失敗、進入 retry 排程。AI inference 不是同步的東西——它是會跑幾十秒、會 tool call、會 streaming、會 retry 上游 LLM API 的東西。把這兩個塞在同一個 handler 裡,時間就會錯位。

具體數字:

ProviderTimeout
Stripe10s
GitHub10s
Shopify5s
Slack3s
Twilio15s
Discord3s

典型 AI handler latency:

場景Latency
OpenAI tool call(GPT-4o 含 1-2 tools)30-60s
LangChain ReAct agent(3-5 步)45-90s
LangGraph 含 retry 與 conditional edge60-120s
Gemini long context(>50K tokens)60s+
Anthropic computer use 一輪30-90s
Replicate video generation2-5min

兩張表交集是空的。沒有一個 AI handler latency 落在任何 webhook timeout 以內。這不是「優化一下就好」的問題,是兩種系統的時間尺度根本不在同一個量級。

而 webhook provider 的 retry 不是 best effort,是激進的:Stripe 預設 retry 3 天、GitHub 8 小時、Shopify 48 小時,都用 exponential backoff。也就是說只要你的 handler 第一次沒在 timeout 內回 200,同一個 event 會被 deliver 第二次、第三次、第八次——而你的 handler 第一次其實已經跑完了,第二次又跑了一遍,狀態被改了兩次。

常見的錯誤解法

自己寫 queue。 開 Redis、開 BullMQ、開 worker process、寫 retry 邏輯、寫 dead letter、寫 monitoring。技術上能解,但你是要寫產品還是要寫 queue infra?而且你還沒處理 signature verification、replay、log 保留、circuit breaker。一個禮拜過去你不在做你原本要做的東西。

用 setTimeout / waitUntil 假裝 async。 Vercel 的 waitUntil 看起來像 fire-and-forget,但執行環境會被回收、log 看不到、retry 沒人管、failure 你不知道。這只是把問題藏起來。

把 Vercel function timeout 改到 300 秒。 你付了錢,但 Stripe 的 10 秒沒變。Stripe 該 retry 還是會 retry,你的 handler 反而跑更久、燒更多錢、duplicate fulfillment 反而更多。

換 provider 給更長 timeout。 Provider 不會為你改 timeout。Stripe 10 秒已經是業界寬鬆的——Slack 3 秒、Discord 3 秒。這條路沒有出口。

正確 pattern:edge ack + async deliver

把 webhook 處理拆成兩段:

  1. Edge layer——在 50ms 內驗 signature、把 payload 持久化、回 200 給 provider
  2. Async deliver——從持久化層拉 payload,慢慢跑 AI handler,跑完寫結果、失敗就 retry,跟 provider 的 timeout 完全解耦

這個 pattern 把「provider 的時間預算」和「你的 handler 的時間預算」隔開。Provider 在 50ms 內拿到 200,永遠不會 retry。你的 handler 可以跑 60 秒、120 秒、5 分鐘,都不關 provider 的事。Idempotency 由 edge layer 的 event id 保證——同一個 event 不會被 deliver 兩次給你的 handler。

關鍵是 edge layer 必須先持久化、後 ack。如果只 ack 不存,當你的 handler 跑到一半 crash,event 就消失了。如果先存後 ack,crash 之後 retry 系統會把同一個 event 再 deliver 一次給 handler(idempotent),但對 provider 永遠是「我已經收到了」。

實作示範

Before——同步處理,AI handler 跑超過 10 秒,Stripe 觸發 retry:

// app/api/webhook/stripe/route.ts
export async function POST(req: Request) {
  const event = await verifyStripeSignature(req)

  // 這裡會跑 45 秒,Stripe 10 秒後 timeout、retry
  const decision = await openai.beta.threads.runs.createAndPoll(...)
  await provisionFromDecision(decision)
  await sendOnboardingEmail(decision)

  return new Response(null, { status: 200 })
}

After——把 Stripe webhook URL 換成 https://in.anyhook.net/you/stripe,AnyHook 在 50ms 內 ack Stripe,然後 async deliver 到你的 handler。你的 handler 程式碼幾乎一樣,只是現在不再被 10 秒綁住:

// app/api/webhook/stripe/route.ts
// 收 AnyHook 轉發過來的 event;handler 想跑多久跑多久
export async function POST(req: Request) {
  const event = await verifyAnyHookSignature(req)  // 一個 header

  // 跑 45 秒沒關係——Stripe 那邊已經收到 200 了
  const decision = await openai.beta.threads.runs.createAndPoll(...)
  await provisionFromDecision(decision)
  await sendOnboardingEmail(decision)

  return new Response(null, { status: 200 })
}

差異不在 handler 程式碼,差異在請求是誰送來的、超時預算是誰的。原本 Stripe 直接打你的 handler、你揹 Stripe 的 10 秒預算;現在 AnyHook 揹 Stripe 的 10 秒(用 50ms 解決),你揹的是 AnyHook 給你的 60-300 秒預算(看 plan)。

AnyHook 是這個 pattern 的工具

AnyHook 就是上面講的「edge ack + async deliver」的具體實作——一個 webhook relay,把這層 infra 抽出來變成換一個 URL 就接得起來的服務。Inbound 在 Cloudflare Workers 跑簽名驗證跟持久化、50ms 內 ack provider;Outbound 走 QStash push queue 給你的 handler,失敗自動 retry,每次 retry 都重新簽 AnyHook-Signature

兩條使用路徑:

  • Self-host——repo 在 github.com/gba3124/anyhook,Apache 2.0,Docker compose 跑得起來
  • Cloud——anyhook.net,free tier 一個月 3,000 events、不需信用卡

我把它開源是因為 webhook 中繼不該是個 vendor lock-in 的東西。Cloud 版本存在是為了養 self-host 版本的開發。

延伸閱讀

針對個別 AI / inference provider 的 webhook 設定指南:

如果你正在 ship 一個會用 LLM 處理 Stripe / GitHub / Shopify webhook 的產品,多花 30 秒看看——不適合也沒關係。重點是這個雙重扣款的痛在 2026 年只會越來越多,因為 handler 只會越跑越久。

All postsMay 24, 2026 · 8 min

Stop losing webhooks.

Change one URL. Get retries, event log, and one-click replay.