Webhookの落とし穴:開発者がハマる「負の連鎖」を回避せよ
The Valley of Webhooks
The Valley of Webhooks
Webhookの「谷」へようこそ。多くの開発者がAPI統合の際に直面する、避けては通れない技術的課題についての深い洞察です。シンプルに見えるプッシュ通知の仕組みも、スケールし始めると複雑な依存関係やエラーハンドリングという深い谷底へと突き落とされます。本稿では、安定したWebhook実装のためのベストプラクティスを解説します。
Webhookは本当に頭の痛い問題だよ。はっきりさせておくと、StripeのEvents APIにはちゃんとカーソルが用意されていて、大規模な利用者の間では長らくそのポーリングが推奨される手法として使われてきたんだ。
Webhookなら、コンシューマーはプロバイダーからの更新を非同期で受け取れる。データに変更がなければ、プロバイダーは何も送ってこないしね。
一方SCROLLの場合、いつ更新を要求するかをコンシューマー側で判断しないといけない。データがいつ変わったかを知る仕組みがない以上、コンシューマーは慎重にならざるを得ないし、常にポーリングして新しいデータを確認し続ける羽目になるよ。
この提案には2つ問題があると思う。1つ目は、コンシューマー・プロバイダー双方にとって無駄なネットワークトラフィックが増えること。2つ目は、データがいつ変わったかを把握できないから、Webhookを使う場合に比べて、コンシューマー側のローカルデータとプロバイダー側のモデルとのラグが大きくなってしまうことだね。
Webhookはシンプルでどこでも使える。それが長所でもあり、同時に欠点でもあるんだ。だからこそ、本来あまり向いていない用途(ステート同期とか)にも無理やり使われてしまうわけだ。
こうした弱点を補うために、我々[1]はFIFOエンドポイントやポーリングエンドポイント、それに「Svix Stream」と呼んでいるものを追加して、順序を保証した状態同期を実現できるようにしたんだ(それぞれ一長一短あるけどね)。これなら、自分のユースケースに最適な方法でイベントを消費できる。状態同期をもっと簡単にできるよう、現在さらに開発を進めているところだ。Webhookでみんながどんな課題を抱えているのか、ぜひ聞かせてもらいたいな。もっと良くしていきたいから。
OP(投稿者)へ:君の考えを詳しく聞かせてほしいな。あとでメールを送るよ。
追伸:もし知らなければ「Standard Webhooks[2]」をチェックしてみて。OpenAIやAnthropic、Googleなど多くの企業が採用している、署名検証を支援するための仕様だよ。Webhookの課題を一つずつ着実に解決していこうとしているんだ。
1: Svix(投稿でも触れられている)のファウンダーです。Webhookインフラのサービスを提供しています。
最近Quickbooks APIで全く同じ目に遭ったよ。レスポンスもWebhookも全く信用できないんだ。
例えばユーザーや請求書を作成する時、エラーが返ってくるのに実際はエンティティが作成されている、なんてことがよくある。だから、全部作った後に手動でちゃんと作成されたか確認しないといけない。
それに、Quickbooksは更新に時間がかかることもあって、裏で何かの処理をしてる間は会社ファイルがロックされちゃうんだ。そのせいですぐに存在チェックができないし、チェック自体がエラーになったりタイムアウトしたりすることもある。結局、相手のDBとこちらのDBが整合するまで無限にチェックし続けるしかない。でも、1分間に何千件もトランザクションがあると、その状態には一生たどり着けない。永遠に追いつこうとして追いつけない状態が続くんだ。
Quickbooksのサポートに問い合わせたら「そちらのシステムで正しく作成されているか確認するのは君たちの仕事だ」とまで言われたよ。
一体どうして、こんな信頼できないシステムを使い続けるような状況になってしまったんだろう?
Webhookを使った状態同期の問題をうまくまとめているね。提案されているSCROLLというプロトコルが、私がこの11月のIETF 127に持ち込む予定の「Braid-HTTP Subscriptions」というIETFドラフトと驚くほど似ていることに気づいたよ。
どちらのドラフトもGETリクエストにヘッダーを付けてサブスクリプションを要求する形だ。
Scrollリクエスト:
GET /scroll/feed/customers
Prefer: stream
Braidリクエスト:
GET /customers
Subscribe:
どちらもGETレスポンスを開いたままにしてイベントをストリーミングする仕組みだけど、SCROLLはapplication/x-ndjsonで返すのに対し、Braidは209 Multiresponseとcontent-type application/http-historyを使う。これならJSONだけでなく、CSVやPNG、XML、HTML、プレーンテキストなどあらゆるメディアタイプに対応できるんだ。
著者は普及の難しさに言及しているけど、Webhookが普及しているのは「ごく普通のHTTP」だからだよ。普及させるなら、これをごく普通のHTTPに組み込む必要がある。IETFに働きかけて、HTTPを汎用的に拡張して状態同期をサポートさせるべきだ。そうすれば特定のメディアタイプやURL(/scroll/*のような)に依存せず、すべてのHTTPライブラリやツールでそのまま使えるようになる。そうすれば、同じ同期ロジックを何度も何度も再実装しなくて済むようになるはずさ。
興味があったらぜひ連絡してね!
Gerardもさらっと触れているけど、Webhookのもう一つの大きな問題はローカル開発だね。トンネルを使えばいいという意見もあるけど、チームのエンジニア全員がそれぞれのトンネルURLを追加しなきゃいけない。認証や決済のようにプラットフォームを唯一の正(Source of Truth)として使う場合、さらに問題が大きくなる。WorkOSの場合は、チーム全員が共有のデベロップメント・サンドボックスを使って開発するんだけど、全員のローカル環境が常に立ち上がっているわけじゃないから、Postgresの認証情報とサンドボックスが同期されずに問題が起きるんだ。だから結局Events APIを使うことになるんだけど、WorkOSはデータを90日間しか保持してくれない(しかも一度に最大30日分しか取れないから3回もコールが必要)。結局、State APIですべてのデータをロードしてからEvents APIを動かすっていう、めちゃくちゃなやり方になってしまう。
Xでこの件について議論しようとしたら、WorkOSのCEOがDMに持ち込んでおきながら結局何も助けてくれなかったよ。https://x.com/grinich/status/1913035839866835297?s=20
提案された解決策だと、イベントの頻度に関わらず全コンシューマーがサーバーとの持続的な接続を維持することになるよね。イベントの量が非常に多い場合を除くと、効率が悪そう。多くのCDNには接続維持時間の制限があるし、データプロバイダー側も持続的な接続を歓迎しないだろうな。
挙げられている問題(署名、重複排除、バッファリング、ブートストラップ、cron)のうち、署名とブートストラップ以外は、Webhookのペイロードにカウンターを含めるだけで解決できるよ。カウンターをインクリメントしていけば、受信したWebhookのカウンターが一致しない場合に、コンシューマーがEvents APIで足りないデータを取得すればいいだけだ。
著者が言うように、プロバイダーが「少なくとも1回は届ける」と言うだけでは不十分だという点には同意するよ。アーキテクチャ図をわざわざ描かなくても済むような解決策が必要だね。
ブートストラップに関しては、bulk events APIがあったほうがいい。1回ずつリクエストしなくて済むようにね。after/cursorページネーションもあれば完璧だ。社内のKafkaでうまくいく手法が、インターネットを介したサービス間通信でそのままうまくいくとは限らないからね。
SCROLLで2つの問題を同時に解決しようとしているように見えるな。
Webhookは「リアルタイム」イベントの部分は概ねこなせる。もちろん問題はあるけど、他の手法に変えたからといって魔法のように解決するわけじゃない。分散システムのリアルタイム通信は難しいし、その目的においてWebhookは十分優秀だよ。
ログイベントのDBについては、個人的には好きな時にサクッと落とせるSQLiteファイルが欲しいね。パースが必要なCSVやJSONなんていらない。SQLiteをそのまま頂戴!それだけで感謝するよ。最新版をまるごとコピーするだけで十分だし、イベント量に合わせて直近90日や365日分に制限してもいい。とにかく取得するたびに全件送ってくれればいいんだ。もし別のDBと同期するために差分が必要なら、それはこっちの都合で何とかする。とにかくすべての行に不変の識別子さえつけてくれればそれでいいよ。
WebhookよりカーソルベースのページネーションAPIの方がずっと好みだな。明らかに欠点は、429エラーを避けるために適切なポーリング頻度が必要で、その結果、新しいイベントへの反応が遅れることだ。
だからWebhookにもまだ役割はあると思う。変更があったことを通知するだけのシンプルな「合図」として使い、標準の低頻度ポーリングを補完するような使い方がベストじゃないかな。
これなら良いとこ取りができる。
「今はどこも提供していない」なんてことはないよ。不動産物件のリスティングサービスはみんなやっている(https://www.reso.org/reso-web-api/ )。