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

Ashley Innocent

Ashley Innocent

20 3月 2026

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

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

違いを理解する

ポーリング: クライアントが「何か更新はありますか?」と繰り返し尋ねます。 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);

ポーリングのパターン:

シンプルなポーリング:

GET /api/v1/orders/123
# Returns current order state

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

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

# Returns 304 Not Modified if unchanged
# Returns 200 with new data if changed

Sinceベースのポーリング:

GET /api/v1/orders/123/events?since=1710331200
# Returns events since timestamp

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で応答

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

以下に適しています:

例:

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

Webhookを使用する場面

以下に適しています:

例:

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

比較表

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

ポーリングの実装

基本的なポーリング

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}`);
});

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

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
  }
);

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();
}

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');
}

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)
  );
}

ローカルでの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"]
  }'

信頼性の高い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}`);
  }
}

受信側(クライアント)

// 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
  }
}

ハイブリッドアプローチ

重要な更新には、ポーリングと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'
      });
    }
  }
}

よくある質問

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連携をテストしてください。

ApidogでAPIデザイン中心のアプローチを取る

APIの開発と利用をよりシンプルなことにする方法を発見できる