AI InfrastructureMay 24, 20268 min read

為什麼你的 AI agent 需要 webhook relay

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

SYNCHRONOUS · ONE BUDGET, SHAREDStripeverify + AI handler45s20s budget ends heretimeout → retry → duplicateEDGE ACK + ASYNC DELIVER · TWO BUDGETSStripeedgepersist + 200~0.4squeueretrieshandler45s, finethe provider's clock stops at the edge
同一個 handler,差別只在超時預算算在誰頭上。

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

打開 Stripe dashboard,同一個 pi_xxxpayment_intent.succeeded 被 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,時間就會錯位。

具體數字(完整版與出處在各家 provider 行為對照表):

ProviderTimeout
Slack3s
Discord3s
Shopify5s
GitHub10s
Twilio15s
Stripe未公布,實務上抓 20s

典型 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 以內。這不是優化一下就能解決的事,兩種系統的時間尺度根本不在同一個量級。

而多數 provider 的 retry 相當積極:Stripe 用 exponential backoff 重試最多 3 天,Shopify 在 4 小時內重試 8 次。只要你的 handler 第一次沒在 timeout 內回 200,同一個 event 就會被 deliver 第二次、第三次、第八次。而你的 handler 第一次其實已經跑完了,第二次又跑了一遍,狀態被改了兩次。

(GitHub 是例外,它完全不自動重送,失敗就是失敗,要嘛你自己去 redeliver、要嘛事件就這樣沒了。這不會比較好,只是壞的方式不一樣。)

常見的錯誤解法

自己寫 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 給的那 20 秒沒變。Stripe 該 retry 還是會 retry,你的 handler 反而跑更久、燒更多錢,duplicate fulfillment 也更多。

換一個 timeout 比較長的 provider。Provider 不會為你改 timeout,而且 Stripe 那 20 秒在業界已經算寬鬆的了,Shopify 5 秒、Slack 跟 Discord 都只有 3 秒。這條路走不通。

正確 pattern:edge ack + async deliver

把 webhook 處理拆成兩段。

第一段是 edge layer:在 50ms 內驗 signature、把 payload 持久化、回 200 給 provider。第二段是 async deliver:從持久化層拉 payload,慢慢跑 AI handler,跑完寫結果,失敗就 retry,跟 provider 的 timeout 完全解耦。

SYNCHRONOUS · ONE BUDGET, SHAREDStripeverify + AI handler45s20s budget ends heretimeout → retry → duplicateEDGE ACK + ASYNC DELIVER · TWO BUDGETSStripeedgepersist + 200~0.4squeueretrieshandler45s, finethe provider's clock stops at the edge

這樣 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(所以 handler 要 idempotent),但對 provider 來說永遠是「我已經收到了」。

實作示範

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

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

  // 這裡會跑 45 秒,Stripe 20 秒後 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 程式碼幾乎一樣,只是不再被那 20 秒綁住:

// 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 的 20 秒;現在 AnyHook 揹那 20 秒(用 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

Cloud 版在 anyhook.net,free tier 一個月 3,000 events,不需要信用卡。

有一件事值得先講清楚:relay 服務本身不是開源的。開源的是簽章驗證函式庫跟 MCP server,github.com/gba3124/anyhook-mcp,Apache 2.0。這樣安排的理由是驗證這一層不該有 vendor lock-in,你要能自己驗我們簽的每一次投遞,不必相信我們的說法。

延伸閱讀

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

如果你正在 ship 一個會用 LLM 處理 Stripe / GitHub / Shopify webhook 的產品,這個坑值得先看一眼,不適合也沒關係。Handler 只會越跑越久,所以雙重扣款這件事在 2026 年只會越來越常見。

All postsMay 24, 2026 · 8 min

Stop losing webhooks.

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