ほかの Webhook の送信元は、数字を示して、それに合わせることを求めます。Shopify は 5 秒、GitHub は 10 秒、Slack は 3 秒で、どれも Webhook プロバイダー別リファレンスにまとめています。文句は言えても、変えることはできません。
Twilio は例外です。タイムアウトを Webhook 自体のパラメーターとして公開していて、リトライ回数と、どの失敗をリトライするかのポリシーも設定できます。ダイヤルは 5 つあり、どれもデフォルト値付きで提供されていますが、ほとんどの連携では誰も触ったことがありません。
これはどちらの方向にとっても、もったいない話です。最もよくある 2 つのケースのどちらでも、デフォルト値が合っていないからです。
TL;DR
ct接続タイムアウト:デフォルト 5000ms、範囲は 100〜10000rt読み取りタイムアウト:デフォルト 15000ms、範囲は 100〜15000ttリトライを含む合計時間:デフォルト 15000ms、範囲は 100〜15000rcリトライ回数:0〜5、デフォルト 1rpリトライポリシー:どの種類の失敗をリトライするか- 上限は固定です。いくら払っても、15 秒を超える読み取りタイムアウトは手に入りません
タイムアウトは 1 つではなく 3 つ
多くの人は、Webhook のタイムアウトを「送信元がレスポンスをどれだけ待つか」という 1 つのイメージでとらえています。Twilio はそれを 3 つの段階に分けていて、どれに引っかかっているかがわかると、診断がまったく変わります。
ct は「Twilio がサーバーへの TCP 接続を確立するまで待つ時間(ミリ秒)」です。デフォルトは 5000ms、最大は 10000ms です。これに引っかかるのは、Twilio がソケットをまったく開けなかったということです。原因は DNS、TLS、ファイアウォール、あるいは完全にダウンしたオリジンです。ハンドラーが遅いせいでは決してありません。ハンドラーはまだ動いていないからです。
rt は「Twilio が Webhook リクエストを送った後、HTTP レスポンスのパケットをそれぞれ待つ時間(ミリ秒)」です。デフォルトは 15000ms、最大も 15000ms です。一般にタイムアウトと言うときに指しているのはこれで、ハンドラーが実際に使える時間です。
tt は「リトライを含む、すべてのタイムアウトに許される合計時間」です。デフォルトは 15000ms、最大も 15000ms です。一連の試行全体の上限で、多くの人がつまずくのはこのパラメーターです。リトライにも制約がかかるからです。
tt と rt は、デフォルト値も最大値も同じであることに注目してください。提供時のデフォルトのままだと、読み取りタイムアウト 1 回で合計時間をすべて使い切り、設定したつもりのリトライは実行される機会がありません。リトライに意味を持たせたいなら、rt を下げて、tt の中に 2 回以上の試行が収まるようにする必要があります。
この相互作用こそ、ここでいちばん理解しておくべきことです。パラメーターを個別に読んでいるだけでは気づきません。
リトライが実際に起きるように設定する
ハンドラーが普段 400ms で応答し、あきらめる前に 2 回リトライしたいとします。
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
これで、応答しなくなったハンドラーは 15 秒ではなく 3 秒で見切られ、リトライは同じインシデントが続いているうちに届きます。デフォルトのままだと、応答しないリクエストが 1 件あるだけで 15 秒をすべて使い切り、設定したリトライは何の知らせもなく一度も実行されません。
リトライポリシーは意図して決める価値があります。4xx をリトライするのはたいてい間違いです。400 や 404 はリクエストそのものが受け付けられないという意味で、もう一度送っても同じ答えが返ってくるからです。リトライするのは、接続タイムアウト、読み取りタイムアウト、そして 5xx です。一時的である可能性が高いのはこれらの失敗です。これは「Webhook リトライの指数バックオフとジッター」と同じ考え方を、それを設定で表現できる送信元に当てはめたものです。
もう半分はフォールバック URL
Twilio のドキュメントは、「サーバーで障害が起きたときに、フォールバック URL へのフェイルオーバーを速めるために」タイムアウトを短く設定するよう勧めています。
フォールバック URL は、プライマリが失敗したときに Twilio が呼び出す別のエンドポイントです。主要な送信元が提供するものの中では、組み込みのサーキットブレーカーにいちばん近い機能です。ただし役に立つのは、発信者がまだ電話口にいるうちにそこへ到達できるほど、タイムアウトが短い場合だけです。決済の Webhook とは違い、音声やメッセージングで rt と ct が重要になるのはまさにこのためです。
フォールバック先には、プライマリと同じ理由では失敗しえないものを指定してください。同じホスト上にあり、同じロードバランサーの後ろにあり、同じデータベースを共有するフォールバックは、フォールバックではありません。オブジェクトストレージから配信する静的な TwiML なら、フォールバックになります。
ダイヤルでは解決できないケース
Twilio の設定では救えない種類のケースがあり、それがますます一般的になっています。
SMS が届き、その返信を LLM に書かせたいとします。その呼び出しには 20〜60 秒かかります。読み取りタイムアウトの上限は 15 秒で、引き上げることはできません。rt、tt、rc をどう組み合わせても、60 秒かかるハンドラーを最大 15 秒の中に収めることはできません。
答えは 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" },
});
}
ワーカーは、回答ができた時点で messages.create を呼び出します。ユーザーには 1 分以内に返信が届きますが、テキストでのやり取りならそれが普通です。Twilio から見れば、40 ミリ秒でレスポンスが返っています。この議論をより広く扱ったのが AI エージェント向けの Webhook の記事です。
音声の場合は、実際の処理が進む間、メッセージを流すか発信者をキューに入れる TwiML を返すのが同じやり方です。無音を聞かされる発信者は、遅い SMS よりも悪い失敗だからです。
署名検証の方式が違う
X-Twilio-Signature は、ボディに対する単純な HMAC ではありません。Twilio は署名対象の文字列を、リクエストの完全な URL と、フォームエンコードのリクエストの場合はキーでソートして連結した POST パラメーターから組み立てます。それを auth token で HMAC-SHA1 にかけ、base64 でエンコードします。
実務上の影響が 2 つあります。
URL が署名の一部なので、URL を書き換えるものはすべて検証を壊します。ポートを取り除くプロキシ、スキームを https から http に変えるロードバランサー、ヘッダーから URL を組み立て直すフレームワークは、どれも正しく見えるのに検証に失敗するリクエストを生みます。本番環境で検証に失敗し、ローカルでは通るなら、コードが署名に使っている URL と、Twilio が呼び出した URL を正確に比べてください。
アルゴリズムは SHA256 ではなく SHA1 で、フォームエンコードのリクエストでは入力が生のリクエストボディではありません。Stripe 方式の検証コードを流用するのではなく、Twilio 自身の validateRequest ヘルパーを使ってください。組み立て方が本当に違うからです。プロバイダーごとの比較は「Stripe、GitHub、Shopify の Webhook 署名を検証する」にあります。
AnyHook の位置づけ
ダイヤルは便利ですが、固定の上限があります。Twilio が待つのは最大でも 15 秒で、正当なハンドラーでもそれ以上かかるものはたくさんあります。その筆頭が LLM の呼び出しです。タイムアウトチェッカーは、ハンドラーの所要時間をこの 15 秒と比べて計測します。各プラットフォームで先に応答する対処法は、「Webhook ハンドラーが LLM を呼ぶ」にあります。
Webhook の URL を in.anyhook.net/you/twilio に向ければ、その上限はもう自分の上限ではなくなります。AnyHook はエッジから約 1 秒で応答し、その後、プランに応じて、そのコンピュート枠の範囲内で 60〜300 秒の持ち時間でハンドラーに配信します。
- エンドポイントへのリトライは指数バックオフで行われ、
ttが許すよりはるかに長い期間続きます - すべてのイベントは配信前に永続化されるので、ハンドラーが失敗してもメッセージは失われず、再送すれば済みます
- 1 回、5 回、20 回連続の失敗で通知し、20 回で自動的に一時停止します
レスポンスに TwiML が必要なら、それは自分のハンドラーで扱ってください。中継サービスが合うのは、Webhook が指示を求めるリクエストではなく、通知である場合です。
まとめ
Twilio は、ほかのどの主要な送信元よりも、Webhook のタイミングを細かく制御させてくれます。そしてデフォルト値は、すぐに応答するハンドラーを前提にしています。rt を下げてリトライが tt に収まるようにし、rp で 4xx をリトライしないようにし、フォールバック URL は障害の起こり方が異なるものに向けてください。
そのうえで、上限は上限として受け入れてください。最大は 15 秒で、ハンドラーに 1 分かかるなら、どんな設定でも届きません。先に応答し、返信は別の経路で送ります。