Forem

Cover image for Webhook vs ポーリング:API連携はどちらが良い?
Akira
Akira

Posted on • Originally published at apidog.com

Webhook vs ポーリング:API連携はどちらが良い?

要約: ポーリングは定期的に更新をチェックします(シンプルですが非効率的)。Webhookはリアルタイムで更新をプッシュします(効率的ですが複雑)。まれなチェックにはポーリングを、リアルタイムの更新にはWebhookを使用してください。Modern PetstoreAPIは信頼性の高いWebhook配信で両方のパターンをサポートしています。

Apidog を今すぐ試してみよう

違いを理解する

ポーリング: クライアントが「何か更新はありますか?」と繰り返し尋ねます。 Webhook: 何かが起こったときにサーバーが「更新があります!」と伝えます。

例え:

  • ポーリング = 1時間ごとに郵便受けを確認する
  • Webhook = 郵便が届くと郵便配達員がドアベルを鳴らす

ポーリング:仕組み

クライアントは変更をチェックするために定期的なリクエストを行います。以下のように実装します。

// 30秒ごとにポーリング
setInterval(async () => {
  const response = await fetch('https://petstoreapi.com/api/v1/orders/123');
  const order = await response.json();

  if (order.status === 'completed') {
    console.log('Order completed!', order);
    clearInterval(pollInterval);
  }
}, 30000);
Enter fullscreen mode Exit fullscreen mode

ポーリングのパターン:

シンプルなポーリング:

GET /api/v1/orders/123
# Returns current order state
Enter fullscreen mode Exit fullscreen mode

条件付きポーリング (ETag):

GET /api/v1/orders/123
If-None-Match: "abc123"

# Returns 304 Not Modified if unchanged
# Returns 200 with new data if changed
Enter fullscreen mode Exit fullscreen mode

Sinceベースのポーリング:

GET /api/v1/orders/123/events?since=1710331200
# Returns events since timestamp
Enter fullscreen mode Exit fullscreen mode

Webhook:仕組み

イベントが発生すると、サーバーはあなたのエンドポイントにHTTP POSTを送信します。基本的なフローは以下の通りです。

// 1. Webhookエンドポイントを登録
POST /api/v1/webhooks
{
  "url": "https://myapp.com/webhooks/petstore",
  "events": ["order.created", "order.completed"],
  "secret": "whsec_abc123"
}

// 2. イベント発生時にサーバーがWebhookを送信
POST https://myapp.com/webhooks/petstore
{
  "id": "evt_123",
  "type": "order.completed",
  "created": 1710331200,
  "data": {
    "orderId": "123",
    "status": "completed",
    "completedAt": "2024-01-01T12:00:00Z"
  }
}

// 3. Webhookを検証して処理
// 200 OKで応答
Enter fullscreen mode Exit fullscreen mode

ポーリングを使用する場面

以下に適しています:

  • まれなチェック(1時間に1回など)
  • 少数のリソース
  • シンプルな実装
  • クライアントを制御している場合
  • テストとデバッグ

例:

  • 日次レポートのステータス確認
  • 数分ごとの連絡先の同期
  • サーバーの健全性監視
  • 支払いステータスの確認(まれな場合)

ポーリングが適切な場合:

  • 更新がまれな場合
  • わずかな遅延が許容される場合
  • シンプルな実装を望む場合
  • リソースが小さい場合

Webhookを使用する場面

以下に適しています:

  • リアルタイムの更新
  • 監視するリソースが多い場合
  • 時間制約のあるイベント
  • サードパーティとの連携
  • 高頻度の更新

例:

  • 支払いの確認
  • チャットメッセージ
  • 株価アラート
  • 注文ステータスの変更
  • CI/CD通知

Webhookがより優れている場合:

  • 更新が即時である必要がある場合
  • ポーリングでは非効率的な場合
  • 多くのクライアントが同じリソースを監視している場合
  • サーバーの負荷を軽減したい場合

比較表

<!--kg-card-begin: html-->



















































要因 ポーリング Webhook
レイテンシ ポーリング間隔まで リアルタイム
サーバー負荷 高い(空のリクエストが多い) 低い(実際のイベントのみ)
複雑さ シンプル 複雑
信頼性 高い(クライアントが再試行を制御) 中程度(再試行ロジックが必要)
セットアップ なし エンドポイントの登録
ファイアウォールの問題 なし(送信のみ) ホワイトリスト登録が必要な場合あり
コスト 高い(リクエストが多い) 低い(リクエストが少ない)
最適な用途 まれなチェック リアルタイムの更新
<!--kg-card-end: html-->

ポーリングの実装

基本的なポーリング

async function pollOrderStatus(orderId, callback) {
  let lastStatus = null;

  const poll = async () => {
    try {
      const response = await fetch(`https://petstoreapi.com/api/v1/orders/${orderId}`);
      const order = await response.json();

      // Only callback if status changed
      if (order.status !== lastStatus) {
        lastStatus = order.status;
        callback(order);
      }

      // Stop polling if terminal state
      if (['completed', 'cancelled'].includes(order.status)) {
        return;
      }

      // Continue polling
      setTimeout(poll, 5000);
    } catch (error) {
      console.error('Polling error:', error);
      setTimeout(poll, 30000); // Back off on error
    }
  };

  poll();
}

// Usage
pollOrderStatus('order-123', (order) => {
  console.log(`Order status: ${order.status}`);
});
Enter fullscreen mode Exit fullscreen mode

スマートポーリング(指数的バックオフ)

async function smartPoll(url, callback, options = {}) {
  const {
    maxRetries = 10,
    initialInterval = 1000,
    maxInterval = 60000,
    stopCondition = () => false
  } = options;

  let retries = 0;
  let interval = initialInterval;
  let lastData = null;

  const poll = async () => {
    try {
      const response = await fetch(url);
      const data = await response.json();

      // Callback if data changed
      if (JSON.stringify(data) !== JSON.stringify(lastData)) {
        lastData = data;
        callback(data);
      }

      // Stop if condition met
      if (stopCondition(data)) {
        return;
      }

      // Reset interval on successful request
      interval = initialInterval;

    } catch (error) {
      retries++;
      if (retries >= maxRetries) {
        throw new Error('Max retries exceeded');
      }
    }

    // Schedule next poll with exponential backoff
    setTimeout(poll, interval);
    interval = Math.min(interval * 2, maxInterval);
  };

  poll();
}

// Usage: Poll order until completed
smartPoll('https://petstoreapi.com/api/v1/orders/123',
  (order) => console.log('Order:', order),
  {
    stopCondition: (order) => ['completed', 'cancelled'].includes(order.status),
    initialInterval: 2000,
    maxInterval: 30000
  }
);
Enter fullscreen mode Exit fullscreen mode

ETagを使用したポーリング

async function pollWithEtag(url, callback) {
  let etag = null;

  const poll = async () => {
    const headers = {};
    if (etag) {
      headers['If-None-Match'] = etag;
    }

    const response = await fetch(url, { headers });

    if (response.status === 304) {
      // Not modified, continue polling
      setTimeout(poll, 30000);
      return;
    }

    const data = await response.json();
    etag = response.headers.get('etag');

    callback(data);
    setTimeout(poll, 30000);
  };

  poll();
}
Enter fullscreen mode Exit fullscreen mode

Webhookの実装

Webhookの登録

// Register webhook endpoint
async function registerWebhook(url, events) {
  const response = await fetch('https://petstoreapi.com/api/v1/webhooks', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}`
    },
    body: JSON.stringify({
      url,
      events,
      secret: generateSecret()
    })
  });

  return response.json();
}

function generateSecret() {
  return 'whsec_' + crypto.randomBytes(32).toString('hex');
}
Enter fullscreen mode Exit fullscreen mode

Webhookの受信

const express = require('express');
const crypto = require('crypto');
const app = express();

// Raw body parser for signature verification
app.use('/webhooks', express.raw({ type: 'application/json' }));

app.post('/webhooks/petstore', async (req, res) => {
  const signature = req.headers['x-petstore-signature'];
  const body = req.body;

  // Verify signature
  const isValid = verifySignature(body, signature, process.env.WEBHOOK_SECRET);

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(body.toString());

  // Process event
  switch (event.type) {
    case 'order.created':
      await handleOrderCreated(event.data);
      break;
    case 'order.completed':
      await handleOrderCompleted(event.data);
      break;
    case 'order.cancelled':
      await handleOrderCancelled(event.data);
      break;
  }

  // Acknowledge receipt
  res.status(200).json({ received: true });
});

function verifySignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}
Enter fullscreen mode Exit fullscreen mode

ローカルでのWebhookテスト

# Use ngrok to expose local endpoint
ngrok http 3000

# Register ngrok URL as webhook endpoint
curl -X POST https://petstoreapi.com/api/v1/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "url": "https://abc123.ngrok.io/webhooks/petstore",
    "events": ["order.created", "order.completed"]
  }'
Enter fullscreen mode Exit fullscreen mode

信頼性の高いWebhook配信

Webhookは失敗する可能性があります。必ず再試行ロジックを実装しましょう。

送信側(サーバー)

// Queue webhooks for delivery
const webhookQueue = [];

async function sendWebhook(event) {
  const webhooks = await db.webhooks.findMany({
    where: { events: { contains: event.type } }
  });

  for (const webhook of webhooks) {
    webhookQueue.push({
      webhook,
      event,
      attempts: 0,
      nextAttempt: Date.now()
    });
  }

  processQueue();
}

async function processQueue() {
  const now = Date.now();

  for (const item of webhookQueue) {
    if (item.nextAttempt > now) continue;

    try {
      await deliverWebhook(item);
      // Remove from queue on success
      webhookQueue.splice(webhookQueue.indexOf(item), 1);
    } catch (error) {
      // Schedule retry with exponential backoff
      item.attempts++;
      item.nextAttempt = now + getBackoff(item.attempts);

      if (item.attempts >= 5) {
        // Mark as failed after 5 attempts
        await markWebhookFailed(item);
        webhookQueue.splice(webhookQueue.indexOf(item), 1);
      }
    }
  }

  setTimeout(processQueue, 5000);
}

function getBackoff(attempt) {
  // 1min, 5min, 15min, 1hr, 4hr
  const delays = [60000, 300000, 900000, 3600000, 14400000];
  return delays[attempt - 1] || delays[delays.length - 1];
}

async function deliverWebhook({ webhook, event }) {
  const signature = generateSignature(event, webhook.secret);

  const response = await fetch(webhook.url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Petstore-Signature': signature,
      'X-Petstore-Event': event.type
    },
    body: JSON.stringify(event),
    timeout: 10000
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

受信側(クライアント)

// Idempotent webhook handling
const processedEvents = new Set();

app.post('/webhooks/petstore', async (req, res) => {
  const event = JSON.parse(req.body.toString());

  // Skip if already processed (idempotency)
  if (processedEvents.has(event.id)) {
    return res.status(200).json({ received: true });
  }

  try {
    await processEvent(event);
    processedEvents.add(event.id);

    // Clean up old event IDs (keep last 1000)
    if (processedEvents.size > 1000) {
      const arr = Array.from(processedEvents);
      arr.slice(0, arr.length - 1000).forEach(id => processedEvents.delete(id));
    }

    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook processing error:', error);
    // Return 5xx to trigger retry
    res.status(500).json({ error: 'Processing failed' });
  }
});

async function processEvent(event) {
  // Process the event
  switch (event.type) {
    case 'order.created':
      await handleOrderCreated(event.data);
      break;
    // ... handle other events
  }
}
Enter fullscreen mode Exit fullscreen mode

ハイブリッドアプローチ

重要な更新には、ポーリングとWebhookの両方を使用します。以下のように組み合わせて実装可能です。

class OrderMonitor {
  constructor(orderId, callback) {
    this.orderId = orderId;
    this.callback = callback;
    this.pollInterval = null;
  }

  async start() {
    // Start with polling for immediate feedback
    this.startPolling();

    // Register webhook for real-time update
    await this.registerWebhook();
  }

  startPolling() {
    this.pollInterval = setInterval(async () => {
      const order = await this.fetchOrder();
      this.callback(order);

      if (['completed', 'cancelled'].includes(order.status)) {
        this.stop();
      }
    }, 10000);
  }

  async registerWebhook() {
    const response = await fetch('https://petstoreapi.com/api/v1/webhooks', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${TOKEN}` },
      body: JSON.stringify({
        url: 'https://myapp.com/webhooks/petstore',
        events: [`order.${this.orderId}`],
        oneTime: true // Auto-delete after first delivery
      })
    });

    this.webhookId = (await response.json()).id;
  }

  stop() {
    if (this.pollInterval) {
      clearInterval(this.pollInterval);
    }
    if (this.webhookId) {
      fetch(`https://petstoreapi.com/api/v1/webhooks/${this.webhookId}`, {
        method: 'DELETE'
      });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

よくある質問

Q: どのくらいの頻度でポーリングすべきですか?緊急性によります。ほぼリアルタイムなら30秒。緊急でないなら5分。鮮度とサーバー負荷のバランスを取ってください。

Q: Webhookエンドポイントがダウンしたらどうなりますか?優れたWebhookプロバイダーは指数的バックオフで再試行します。重複配信に対応するためにべき等性を実装してください。

Q: Webhookを安全にするにはどうすればよいですか?共有シークレットを使用して署名を検証してください。HTTPSのみを使用してください。イベントデータを検証してください。

Q: Webhookを履歴データに使用できますか?いいえ。Webhookは新しいイベント専用です。履歴データにはポーリングまたはバッチAPIを使用してください。

Q: モバイルアプリにはポーリングとWebhookのどちらを使用すべきですか?モバイルにはポーリングがシンプルです。Webhookは仲介としてプッシュ通知が必要です。

Q: Webhookの問題をデバッグするにはどうすればよいですか?テストにはwebhook.siteのようなツールを使用してください。すべてのWebhook配信をログに記録してください。APIでWebhookイベント履歴を提供してください。

Modern PetstoreAPIはポーリングとWebhookの両方をサポートしています。実装の詳細については、Webhookガイドを参照してください。ApidogでWebhook連携をテストしてください。

Top comments (0)