其他每個 Webhook 發送端都只給你一個數字,要你自己接受。Shopify 的五秒、GitHub 的十秒、Slack 的三秒,全都整理在我們的 Webhook 服務商參考資料。你可以抱怨,但改不了。
Twilio 是例外。它把逾時開放成 Webhook 本身的參數,另外還有重試次數,以及哪些失敗值得重試的重試原則。五個旋鈕,出廠都有預設值,而在大多數整合裡,從來沒有人動過。
這在兩個方向上都是錯失的機會,因為預設值對最常見的兩種情況都不對。
重點摘要
ct連線逾時:預設 5000 毫秒,範圍 100 到 10000rt讀取逾時:預設 15000 毫秒,範圍 100 到 15000tt含重試在內的總時間:預設 15000 毫秒,範圍 100 到 15000rc重試次數:0 到 5,預設 1rp重試原則:哪些類型的失敗值得重試- 上限是硬性的。不管付多少錢,讀取逾時都買不到超過 15 秒
三種逾時,不是一種
大多數人對 Webhook 逾時只有一種想像:發送端等回應等多久。Twilio 把它拆成三個階段,知道自己撞上的是哪一個,診斷方向就會完全不同。
ct 是「Twilio 等待與你的伺服器建立 TCP 連線的時間,單位為毫秒」。預設 5000 毫秒,最大 10000 毫秒。撞上它,代表 Twilio 根本開不了 socket,原因是 DNS、TLS、防火牆,或來源伺服器整個當機。絕不會是處理程式太慢,因為處理程式根本還沒執行。
rt 是「Twilio 送出 Webhook 請求後,等待每個 HTTP 回應封包的時間,單位為毫秒」。預設 15000 毫秒,最大 15000 毫秒。大家說逾時,指的就是這個,它也是處理程式真正的回應時限。
tt 是「包含重試在內,所有逾時加總允許的時間」。預設 15000 毫秒,最大 15000 毫秒。這是整個嘗試序列的上限,也是最容易讓人踩雷的參數,因為它連重試也一起限制。
注意 tt 和 rt 的預設值和最大值都一樣。照出廠預設,一次讀取逾時就會用掉整個總時間,你以為設好的重試永遠沒有機會執行。想讓重試有意義,就得調低 rt,讓兩次以上的嘗試放得進 tt。
這個交互作用是這裡最值得理解的一件事,而一個一個看參數時,完全看不出來。
設定到讓重試真的會發生
假設處理程式平常 400 毫秒就會回應,而你希望放棄之前重試兩次。
rt = 3000 // 3s per attempt, generous against a 400ms handler
rc = 2 // two retries
tt = 12000 // room for three attempts plus connect overhead
ct = 3000 // fail fast if the socket will not open
rp = ct,rt,5xx
現在卡住的處理程式會在三秒時被放棄,而不是十五秒,重試也會在同一次事故還沒結束時送到。對照預設值:一個卡住的請求就燒光全部十五秒,設定好的重試則默默地從未觸發。
重試原則值得仔細決定。重試 4xx 通常是錯的,因為 400 或 404 代表請求本身不被接受,再送一次也只會得到同樣的答案。要重試的是連線逾時、讀取逾時和 5xx,這些才是可能只是暫時性的失敗。這和「Webhook 重試的指數退避與隨機抖動」是同一套道理,只是套用在一個讓你用設定表達它的發送端上。
備援網址是另一半
Twilio 的文件建議把逾時設短一點,「以便在你的伺服器服務中斷時,更快切換到備援網址」。
備援網址(fallback URL)是主要端點失敗時,Twilio 會改呼叫的第二個端點,也是主要發送端裡最接近內建斷路器的功能。但前提是逾時要夠短,才能在來電者還在線上時切換過去;這正是為什麼 rt 和 ct 對語音和訊息很重要,對付款 Webhook 卻沒那麼重要。
把它指向一個不會因為同樣原因失敗的地方。放在同一台主機、同一個負載平衡器後面、共用同一個資料庫的備援,不是備援。從物件儲存提供的靜態 TwiML 才是。
旋鈕解決不了的情況
有一類情況是 Twilio 的設定救不了的,而且越來越常見。
一則簡訊進來,你想讓 LLM 寫回覆。這次呼叫要二十到六十秒。讀取逾時的上限是十五秒,而且調不高。沒有任何 rt、tt 和 rc 的組合,能讓一個要跑六十秒的處理程式,塞進十五秒的上限裡。
答案和 Slack 一樣:確認收到和回覆分開做。立刻回傳空的 TwiML,讓 Twilio 的計時停下來,等模型完成後,再透過 Messages API 送出真正的回覆。
export async function POST(req: Request) {
const raw = await req.text(); // raw body, for the signature
if (!verifyTwilioSignature(req.url, raw, req.headers.get("x-twilio-signature"))) {
return new Response("unauthorized", { status: 403 });
}
await queue.publish(raw); // durable, ~10ms
// Empty TwiML: acknowledged, nothing to say yet
return new Response('<?xml version="1.0" encoding="UTF-8"?><Response></Response>', {
headers: { "content-type": "text/xml" },
});
}
接著 worker 拿到答案後呼叫 messages.create。使用者在一分鐘內收到回覆,這對簡訊對話來說很正常。Twilio 看到的回應則只花了 40 毫秒。這個論點更完整的版本,寫在「給 AI agent 用的 Webhook」。
語音的對應做法,是回傳一段 TwiML,在真正的工作進行時播放訊息,或把來電者排入等候佇列,因為讓來電者聽著一片寂靜,比簡訊回得慢更糟。
這裡的簽章驗證不一樣
X-Twilio-Signature 不是對請求主體做的單純 HMAC。Twilio 用完整的請求網址組出要簽章的字串;如果是表單編碼的請求,還會把 POST 參數依鍵名排序後串接上去。接著用你的 auth token 做 HMAC-SHA1,再以 base64 編碼。
實務上有兩個影響。
網址是簽章的一部分,所以任何改寫網址的東西都會讓驗證失敗。拿掉連接埠的代理伺服器、把 scheme 從 https 改成 http 的負載平衡器,或從標頭重建網址的框架,都會產生看起來正常、卻通不過檢查的請求。如果驗證在正式環境失敗、在本機卻通過,就把你的程式碼拿來簽章的網址,和 Twilio 實際呼叫的網址逐字比對。
它是 SHA1,不是 SHA256;而且表單編碼的請求,輸入也不是原始請求主體。直接用 Twilio 自己的 validateRequest 輔助函式,不要拿 Stripe 風格的驗證程式來改,因為兩者的組法真的不同。各服務商的比較,寫在「驗證 Stripe、GitHub 和 Shopify 的 Webhook 簽章」。
AnyHook 的角色
這些旋鈕很有用,但有硬性上限。Twilio 最多只會等十五秒,而很多正當的處理程式需要更久,呼叫 LLM 尤其如此。逾時檢查工具會拿這十五秒替你的處理程式計時,而「你的 Webhook 處理程式呼叫 LLM」有各平台先回應、再處理的解法。
把 Webhook 指向 in.anyhook.net/you/twilio,這個上限就不再是你的上限。AnyHook 在邊緣節點大約一秒就回應,接著把事件傳遞給你的處理程式,依方案並在運算額度內,最多等 60–300 秒。
- 對你的端點的重試採用指數退避,持續時間遠比
tt允許的長 - 每個事件在傳遞之前就已經持久儲存,所以處理程式失敗時,你要做的是重播,而不是面對一則遺失的訊息
- 連續失敗 1、5、20 次時發出失敗警示,達 20 次時自動暫停
如果回應裡需要 TwiML,就留在你自己的處理程式裡。當 Webhook 只是通知、而不是在向你要指示時,Webhook 中繼服務(webhook relay)才是合適的形式。
結論
在 Webhook 的時間控制上,Twilio 給你的空間比其他任何主要發送端都多,而它的預設值是為立刻回應的處理程式設定的。調低 rt,讓重試放得進 tt;把 rp 設成跳過 4xx;把備援網址指向失敗模式不同的地方。
然後接受上限就是上限。十五秒是最大值,如果處理程式需要一分鐘,任何設定都幫不了你。先確認收到,再從另一條管道回覆。