要約: ポーリングは定期的に更新をチェックします(シンプルですが非効率的)。Webhookはリアルタイムで更新をプッシュします(効率的ですが複雑)。まれなチェックにはポーリングを、リアルタイムの更新にはWebhookを使用してください。Modern PetstoreAPIは信頼性の高いWebhook配信で両方のパターンをサポートしています。
違いを理解する
ポーリング: クライアントが「何か更新はありますか?」と繰り返し尋ねます。 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);
ポーリングのパターン:
シンプルなポーリング:
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で応答
ポーリングを使用する場面
以下に適しています:
- まれなチェック(1時間に1回など)
- 少数のリソース
- シンプルな実装
- クライアントを制御している場合
- テストとデバッグ
例:
- 日次レポートのステータス確認
- 数分ごとの連絡先の同期
- サーバーの健全性監視
- 支払いステータスの確認(まれな場合)
ポーリングが適切な場合:
- 更新がまれな場合
- わずかな遅延が許容される場合
- シンプルな実装を望む場合
- リソースが小さい場合
Webhookを使用する場面
以下に適しています:
- リアルタイムの更新
- 監視するリソースが多い場合
- 時間制約のあるイベント
- サードパーティとの連携
- 高頻度の更新
例:
- 支払いの確認
- チャットメッセージ
- 株価アラート
- 注文ステータスの変更
- CI/CD通知
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連携をテストしてください。
