
2026/08/06 0:22
Webhook の谷
RSS: https://news.ycombinator.com/rss
要約▶
日本語翻訳:
本文は、現在の Webhook アーキテクチャが根本的に欠陥を持っており、順序付け、完全性の保証、ならびにブートストラップデータが不足しているため、開発者が再試行キュー、重複除去ストア、署名検証トンネル、毎夜実行される cron ジョブといった複雑なローカル一貫化システムを構築せざるを得ず、データの整合性を維持させられていることを論じている。著者は、1 つの URL ごとにコレクション単位で全状態イベントの変更ログ(順序付きでカーソルアドレス付けされたもの)を提供する標準化されたプロトコルである SCROLL(Synchronized Change Replication Over Line Logs)を提唱している。このアプローチは、誤りが多いエンジニアリングの回避策を、ID キー付けによるブラインドアップサーフトと削除レコードのためのトムストーンを用いた信頼性の高い状態複製手法で置き換えるものであり、これによりデータの整合性が保証される。2007 年の「発信即忘(fire-and-forget)」概念に由来するが、共有コントラクトの欠如のため、業界はローカル最適化の谷底の底に留まっているままであった。しかし、SCROLL はチェックポイント、保存ポリシー、ならびに既存の Webhook システムがリスト API または既存の Webhook からフィードを合成できるシム機能を備えたという重要な新概念を導入する。SCROLL の採用により、カスタムインフラストラクチャの必要性を排除しながら堅牢なデータ同期を可能にし、システムアーキテクチャを著しく単純化するだろう。
本文
Webhook のジレンマ:なぜ「再構築」は避けられないのか?
背景と問題の所在
システムを 3 度目に構築した際、以下の構造を異なる 3 つのサードパーティ・プロバイダに対して作成していたことに気づいた。この仕組みには正式な名称もなくロードマップにも載らないが、必ず同じ道筋を通る。
- 真実の分離: 自社の顧客に関する「真実」は外部のデータベースに眠っている。
- ユーザー情報:アイデンティティ・プロバイダ
- 購読情報:Stripe
- メールバウンス情報:送信側
- ローカルコピーの必要性: 製品がその「真実」を保持するために、Webhook に購読し、常にコピーを保つ必要がある。
- 当初の過信:
- 単なるエンドポイントを構築していると思っていた(JSON をパースして 1 行更新)。
- 半日かからずとも良いと想定していたが、実際はすぐに膨らんだ。
システムが複雑化した理由
単純なエンドポイントが以下の要素を加えられ、週単位の開発に変わった。
- 署名検証の追加
- データベースを変更可能なオープンなエンドポイントにはセキュリティ上の穴があるため。
- 重複削除テーブルの必要性
- 配信は必ず 2 回以上届くため(ドキュメントはこれを「at-least-once」としている)。
- ハンドラのバッファ化
イベントが、指し示すmembership.created
イベントよりも先に現れることがあるため。user.created
- ブートストラップインポーターの導入
- Webhook は購subscribe 後の出来事のみを伝えるため、ライブイベントと競合しロック機構が必要になった。
- reconciling cron の必要性
- 午前 3 時にプロバイダのリスト API をクローリングし、自社のテーブルと照合して不一致を是正するジョブ。
「再構築」の本質:信頼の喪失
この cron は書面による告白であり、**「自分で構築したコピーには信用しないし、それが誤っているかどうかを知る手段もないため、毎晩ゼロから再構築せざるを得ない」**という宣言である。
- ドリフト(ズレ)の発見不能性:
- ドリフトは自覚を告げず、サポートチケットによってしか発見されない。
- 例:顧客が数か月前にキャンセルしていたのに、DB では「アクティブ」のまま (
イベント消失)。customer.subscription.deleted
- ログとダッシュボードの限界:
- 相手側:再試行され破棄されたとして表示されるだけ。
- 自分側:届きもしないリクエストを記録できない。
- デバッグの難しさ:
- 各プロバイダに独自のダッシュボードと Webhook 設定がある。
- 登録方法、イベント定義、環境区分、シークレット保存場所など解釈が異なるため、複数タブを開いたツアーのようなデバッグになりがち。
なぜこれが標準になってしまったのか?
誰かがこれを決めたわけではない。「Webhook」という用語は Jeff Lindsay(2007 年)が考案したものであり、初期の利用例は適していた。
- 本来の目的: 「あることが起こればそれを処理すること」 (Post-receive, CI ビルド起動、領収書送信)。
- 広まった理由: 最も安価な手段(1 つの HTTP POST)であり、コンシューマーもコストを最小限に抑えられたため。
しかし、以下のように2 つの任務が混同された。
| 任務 | 本来の目的 | 私が 3 度も行っていたこと (Webhook の限界) |
|---|---|---|
| 副次的な効果 | 領収書送信、ビルド開始など | × |
| データの正しくコピー | なし | △ (順序性、完全性、ブートストラップ、検証可能性が必要) |
2007 年に選択したツールから、15 年間補償のために苦労してきたことになる。
「谷 (The Valley)」と局所最適解
進化生物学の概念である「適応度ランドスケープ」で考えると理解しやすい。
- 山頂: 良い設計。
- 谷: 悪い状態だが、集団はたまたま立っている斜面を登っていくため抜け出せない。
- 罠: 周囲よりも少し良いだけの小さな山頂(局所最適解)に留まること。高い山頂へ行くには一時的な劣化が必要だが、進化は一時的な劣化を受け入れない。
複製用 Webhook は局所最適解である。その証拠は谷の床に積まれた多くの回避策にある:
- 署名スキーム
- 重複削除ストア
- 冪等なハンドラ
- リトライキューとデッドレターキュー (DLQ)
- Webhook ログと再プレーツールの組み合わせ
- 午前 3 時の reconciling cron
経済性と市場の方向性
多くの企業が「谷」を販売しているが、本質は変わらない。
- Svix, Hookdeck: プラフォーマスで構築の手間を省くため。
- AWS EventBridge + SQS + Lambda: 「谷」そのものをマネージド・サービスとして提供する。
- Fivetran, Airbyte: 疑似的な CDC で、結局は Webhook とリスト API から再構築されたもの。
- ローカルトンネル問題:
などの CLI は、プロバイダ自体が「原生」の配信方向を回避するために開発・維持している(例えが悪い問いに答えている)。stripe listen
しかし、一部のプロバイダは上方を覗いている。
- Stripe:
で順序付きのログを公開。/v1/events - WorkOS: Events API でカーソルによるページネーションを提供し、データ一貫性重視では Webhook より推奨。
- 無関係な生物群が同じ環境圧力を受け、同じ翼(ログと照合)が生じる収斂進化だ。
代替案と限界
新しい設計を検討する前に、置き換え案が提供するべき要件を確認する。
必須要件リスト
- 順序性: バッファなしで変更を適用するため。
- ゼロからの開始: ブートストラップをライブイベントと競合させないため。
- 削除の扱い: 欠如が障害モードにならないため(データとして扱う)。
- 再開可能性: ダウンタイムをデータ損失イベントとしないため。
- 検証手段: クロンのような信頼性の低下を防ぐため。
現在の候補は不十分
- リスト API のポーリング:
に近く、現在状況を再構築できるがレートリミットを燃やす。reconciling cron- 順序性を保証できず、削除されたオブジェクトと存在しなかったオブジェクトを見分けられない。
- マネージド・デリバリー (Svix, EventBridge など):
- プッシュの信頼性を高めるが、ブートストラップや検証も不要である。
- 通知として振る舞うため、データセットのコピーにはならない(谷の床を硬くするだけ)。
第 3 の候補:矢印を反転させる
プロバイダ自身で構築しつつある方向性:プッシュ altogether を停止し、コンシューマー自身がログを読む。
新しい設計:SCROLL (Synchronized Change Replication Over Line Logs)
思考実験として、通知を受ける代わりに「最後にチェックした時点以降に新しい情報を問い合わせる」仕組みを提案する。
仕組みのイメージ
プロバイダが各コレクションに対し、フルステートイベントの順序付きカーソルアドレス付変更ログ (
feed) を提供する。
GET /feed/customers?cursor=01J9XQ4R Prefer: stream 200 OK Content-Type: application/x-ndjson {"cursor":"01J9XR2M","operation":"upsert","object":{"id":"cus_123","plan":"pro"}} {"cursor":"01J9XR2N","operation":"delete","object_id":"cus_099"}
- ストリーミングモード:
で指定すると、コミットするたびに接続を通じて到着する。Prefer: stream - ポーリングモード: 外しておくと、有界なページ(cron から読み取れる)が得られる。
- 共通インターフェース: 同じエンドポイント、同じ API キー、同じカーソル、同じコンシューマーコードで利用可能。
スタックへの影響
このアプローチにより、以下の要素が不要になる:
- 重複削除テーブルの消滅: 各イベントにはオブジェクトの完全な現在の状態が含まれているため、id で鍵付けされたブラインドな
が可能(冪等性)。upsert - 順序バッファの廃止: ログ自体が順序付きであるため。
- ブートストラップインポーターとロック機構の消滅: カーソルなしで同じフィードを読み込み、すぐにライブ変化へ移行可能。
- 消失した削除の排除: トーンストーン(墓標)はログ上のイベントであり、読み取るまで存在する(欠如が障害モードにならない)。
- エンドポイント・署名・トンネルの不要化: 各接続はコンシューマーが initiated するため。
検証の追加
読み込みがログの末尾に達した際、プロバイダが現在の状態の数とチェックサムを知らせることで、複製が正しいことを確認できる(事前の
reconciling cron に代わる)。
第 4 の試み:SCROLL の概要
そのようなフィードがあれば、システム構築は以下の単純なループになる。
- フィードを取得する。
で DB に追加・更新し、upsert
で削除する。delete- 最後のカーソルを保存する。
- 実装コスト: ルートなし、シークレットローテーションなし、キューなし、cron なしの約 20 行。
- 複製の自己証明: 正しさの証明を自身で持ち歩く(JOIN で欠落したデータを見分けられる)。
現状の問題: 今日誰もこれを提供していない。これが落とし穴であり、同時に要点でもある。
SCROLL プロトコルとコミュニティへの要請
このアイデアを存続させるために、RFC 的な意味での draft-00 を公開した:
- 名称:
(SCROLL)Synchronized Change Replication Over Line Logs - 公開先:
welidev.github.io/scroll - 内容: フィード、カーソル、ストリーミングとポーリングモード、チェックポイント、トーンストーン、保持ポリシーの確定。
プロバイダへの提案:
- 既存の Webhook とリスト API からフィードを合成するシャイム(シーム)を提供すること。
結びに
もし「谷」で生活し、重複削除テーブルを書いたり、cron をデバッグしたりしてきたなら、SCROLL を読んでほしい。それが壊れる場所を教えてくれるだろう。
- 異論こそが望ましい回答。
- 沈黙こそが故障モードです。