ポーリング vs. Webhook:通知を受け取る2つの方法
サービス間の連携を初めて構築したとき、デフォルトのアプローチはポーリングでした。30秒ごとにAPIエンドポイントを叩いて、何か変わったか確認し、対応する。確かに動きます。でも数ヶ月本番で運用していると、問題が見えてきます。無駄なAPIコール、レート制限の悩み、そして常にポーリング間隔の半分以上あるレイテンシ。
Webhookはこのモデルを逆転させます。アプリが定期的に「何か起きた?」と尋ねる代わりに、外部サービスが何か起きた瞬間に教えてくれます。1時間ごとにポストを確認しに行くか、郵便配達員がドアベルを鳴らしてくれるか、その違いと同じです。
WebhookはHTTPコールバックです。特定のイベントをトリガーに、あるシステムが指定したURLにPOSTリクエストを送る仕組みです。支払い完了?StripeがあなたのエンドポイントにPOSTします。新しいコミットがプッシュされた?GitHubがCIサーバーにPOSTします。コードがデプロイされた?監視ツールがSlackのWebhook URLにPOSTします。
具体的に比較してみましょう:
# ポーリング方式(30秒ごとに実行)
while True:
response = requests.get('https://api.example.com/orders?status=new')
for order in response.json():
process_order(order)
time.sleep(30)
# Webhook方式(外部サービスから呼び出されるエンドポイント)
@app.route('/webhook/new-order', methods=['POST'])
def handle_new_order():
order = request.json
process_order(order)
return '', 200
ポーリング版は何も起きていなくても実行され続けます。Webhook版は注文が届いたときだけ動きます。トラフィックが少なければ差は小さいですが、スケールすると、1日5万回の無駄なAPIコールとゼロの差になります。
6ヶ月の本番運用で見えたメリット・デメリット
うまくいくこと
- リアルタイム配信 — イベントは発生してからミリ秒以内に届きます。Stripeの決済が成功してから1秒以内に確認メールが送れるようになりました。
- サーバー負荷の軽減 — 無駄なポーリングループがなくなります。実際に処理が必要なときだけサーバーが動きます。
- シンプルなコード — Webhookハンドラーは小さなHTTPエンドポイントだけです。「最終確認時刻」を追跡するためのステートマシンも不要です。
- サードパーティエコシステム — GitHub、Stripe、Shopify、Twilio、Slack——これらはすべてWebhookをネイティブにサポートしています。連携する価値のあるサービスなら、まずWebhookをサポートしています。
辛くなるところ
- エンドポイントが公開されている必要がある — ローカル開発中はこれが面倒です。ローカルホストを公開するために
ngrokやlocaltunnelのようなトンネルツールが必要になります。 - 受信側になる — イベントが発生したときにサーバーがダウンしていると、簡単に再試行できません。多くのサービスは数回リトライしてくれますが、長時間ダウンしているとイベントを失います。
- 重複配信は現実に起きる — ネットワークのタイムアウトがリトライを引き起こします。本番環境で同じ支払いイベントを2回処理したことがあります。べき等性チェックは省略できません。
- セキュリティはあなたの責任 — エンドポイントのURLを知っている人は誰でもPOSTできます。リクエストが期待したソースから来たことを必ず検証する必要があります。
正直なところ、イベント駆動のユースケースではWebhookはポーリングより明らかに優れています。ただし、運用上の責任があなたに移ります。ポーリングは単純で信頼性が高く、Webhookは賢いが注意が必要です。
本番環境での推奨セットアップ
1. Webhookシグネチャを検証する
主要なWebhookプロバイダーはすべてシグネチャヘッダーを提供しています。GitHubはX-Hub-Signature-256、StripeはStripe-Signature(リプレイ攻撃を防ぐタイムスタンプも含む)です。ペイロードを処理する前に必ず検証してください。
import hmac
import hashlib
def verify_github_signature(payload_body: bytes, signature_header: str, secret: str) -> bool:
"""GitHub Webhookのシグネチャを検証する。"""
if not signature_header:
return False
hash_object = hmac.new(
secret.encode('utf-8'),
msg=payload_body,
digestmod=hashlib.sha256
)
expected_signature = 'sha256=' + hash_object.hexdigest()
# タイミング攻撃を防ぐためhmac.compare_digestを使用
return hmac.compare_digest(expected_signature, signature_header)
6ヶ月間の本番トラフィックで、なりすましリクエストはゼロ。エンドポイントに10件ほどの不正なPOSTが来ましたが、ほとんど自動スキャナーでした。HMACチェックがすべて弾いてくれます。
2. 素早く200を返し、非同期で処理する
Webhookエンドポイントは2〜5秒以内に応答する必要があります。それ以上かかると送信側が失敗と判断してリトライします。重い処理をインラインで行わず、ジョブをキューに積んですぐ返してください。
import redis
import json
r = redis.Redis()
@app.route('/webhook/stripe', methods=['POST'])
def stripe_webhook():
payload = request.get_data()
sig_header = request.headers.get('Stripe-Signature')
# まずシグネチャを検証
if not verify_stripe_signature(payload, sig_header):
return '無効なシグネチャ', 400
# キューに積んですぐ返す
event = json.loads(payload)
r.lpush('webhook_queue', json.dumps(event))
return '', 200 # 素早く応答
# 別ワーカーがこのキューを処理
def worker():
while True:
_, job = r.brpop('webhook_queue')
process_stripe_event(json.loads(job))
3. べき等キーで重複を処理する
処理済みのイベントIDを保存してください。イベントを処理する前に、すでに見たことがあるか確認します。
def process_stripe_event(event: dict) -> None:
event_id = event['id'] # 例: 'evt_1ABC123...'
# 処理済みかどうか確認
if r.sismember('processed_events', event_id):
print(f'重複イベントをスキップ: {event_id}')
return
# イベントを処理
if event['type'] == 'payment_intent.succeeded':
handle_payment(event['data']['object'])
# 処理済みとしてマーク(7日後に期限切れ)
r.sadd('processed_events', event_id)
r.expire('processed_events', 604800)
4. ローカル開発にはngrokを使う
# ngrokをインストール
brew install ngrok # macOS
# またはngrok.comからLinux/Windows向けをダウンロード
# ポート5000でローカルサーバーを起動してから外部に公開
ngrok http 5000
# ngrokが以下のような公開URLを発行します:
# https://a1b2c3d4.ngrok.io
# テスト時はこれをWebhook URLとして使用
実装ガイド:自動デプロイのためのGitHub Webhook
実際に使っているパターンです:mainブランチにプッシュ → Webhookが発火 → サーバーがプルして再起動。
ステップ1:Webhookレシーバーをセットアップする
from flask import Flask, request, abort
import hmac, hashlib, subprocess, os
app = Flask(__name__)
SECRET = os.environ['GITHUB_WEBHOOK_SECRET']
@app.route('/deploy', methods=['POST'])
def deploy():
# 1. シグネチャを検証
sig = request.headers.get('X-Hub-Signature-256', '')
body = request.get_data()
expected = 'sha256=' + hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(403)
# 2. mainへのプッシュのみ処理
data = request.json
if data.get('ref') != 'refs/heads/main':
return '無視しました', 200
# 3. デプロイをトリガー(ノンブロッキング)
subprocess.Popen(['/opt/scripts/deploy.sh'])
return 'デプロイ中', 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
ステップ2:GitHubにWebhookを登録する
# GitHub CLIを使う場合
gh api repos/{owner}/{repo}/hooks \
--method POST \
--field 'name=web' \
--field 'active=true' \
--field 'events[]=push' \
--field 'config[url]=https://yourdomain.com/deploy' \
--field 'config[content_type]=json' \
--field 'config[secret]=your_secret_here'
ステップ3:リバースプロキシの背後で動かす
# Nginx設定の一部
location /deploy {
proxy_pass http://127.0.0.1:8080;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
# 不正利用を防ぐためレート制限
limit_req zone=webhook burst=10 nodelay;
}
Webhookが最も効果を発揮する場面
自動デプロイは一例です。ポーリングよりWebhookが一貫して優れていると感じた場面を紹介します:
- 決済処理 — StripeやPayPalは決済成功、返金、紛争のイベントを発火します。確認待ちでユーザーをブロックできない非同期決済フローには不可欠です。
- CI/CDパイプライン — GitHubやGitLabのWebhookはプッシュやPR更新のたびにビルドをトリガーします。どんなポーリング間隔よりも速いフィードバックループを実現できます。
- チャット連携 — SlackやDiscordのインカミングWebhookを使えば、HTTPを1回POSTするだけでどのサービスからも通知を送れます。OAuthもSDKも不要、URLだけでOKです。
- EC(Eコマース) — ShopifyのWebhookは注文が入ったとき、在庫が閾値を下回ったとき、顧客が登録したときに通知します。大規模ストアでは1時間に数千件を処理することもあります。
- 監視アラート — PagerDuty、Datadog、Grafanaはすべてアラート発生時にカスタムエンドポイントへアウトバウンドWebhookを送れます。アラートを社内ツールにルーティングするのに便利です。
個人的なルール:外部APIを1分に1回以上ポーリングしているなら、まずWebhookの代替がないか探します。Webhookエンドポイントを立ち上げるオーバーヘッドは確かにありますが、ほぼ毎回その価値があります。

