Webhook vs Polling: Phương Thức Tích Hợp API Nào Tốt Hơn?

Ashley Innocent

Ashley Innocent

20 tháng 3 2026

Webhook vs Polling: Phương Thức Tích Hợp API Nào Tốt Hơn?

Tóm tắt: Polling kiểm tra cập nhật định kỳ (đơn giản nhưng không hiệu quả). Webhooks đẩy cập nhật theo thời gian thực (hiệu quả nhưng phức tạp). Sử dụng polling cho các lần kiểm tra không thường xuyên, webhooks cho cập nhật thời gian thực. Modern PetstoreAPI hỗ trợ cả hai mô hình với việc gửi webhook đáng tin cậy.

Hiểu về sự khác biệt

Polling: Client hỏi “Có cập nhật nào không?” lặp đi lặp lại. Webhooks: Server nói “Đây là một bản cập nhật!” khi có điều gì đó xảy ra.

Ví dụ minh họa:

Polling: Cách hoạt động

Client thực hiện các yêu cầu định kỳ để kiểm tra các thay đổi.

// Poll mỗi 30 giây
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);

Các kiểu polling:

Polling đơn giản:

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

Polling có điều kiện (ETag):

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

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

Polling dựa trên 'since':

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

Webhooks: Cách hoạt động

Server gửi HTTP POST đến endpoint của bạn khi sự kiện xảy ra.

Quy trình thiết lập:

// 1. Đăng ký webhook endpoint
POST /api/v1/webhooks
{
  "url": "https://myapp.com/webhooks/petstore",
  "events": ["order.created", "order.completed"],
  "secret": "whsec_abc123"
}

// 2. Server gửi webhook khi sự kiện xảy ra
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. Xác minh và xử lý webhook
// Respond with 200 OK

Khi nào nên sử dụng Polling

Thích hợp cho:

Ví dụ:

Polling tốt khi:

Khi nào nên sử dụng Webhooks

Thích hợp cho:

Ví dụ:

Webhooks tốt hơn khi:

Bảng so sánh

Yếu tố Polling Webhooks
Độ trễ Lên đến khoảng thời gian poll Thời gian thực
Tải máy chủ Cao (nhiều yêu cầu rỗng) Thấp (chỉ các sự kiện thực)
Độ phức tạp Đơn giản Phức tạp
Độ tin cậy Cao (client kiểm soát việc thử lại) Trung bình (cần logic thử lại)
Thiết lập Không cần Đăng ký endpoint
Vấn đề tường lửa Không (chỉ đi ra) Có thể cần đưa vào danh sách trắng
Chi phí Cao hơn (nhiều yêu cầu hơn) Thấp hơn (ít yêu cầu hơn)
Tốt nhất cho Kiểm tra không thường xuyên Cập nhật thời gian thực

Triển khai Polling

Polling cơ bản

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

      // Chỉ gọi callback nếu trạng thái đã thay đổi
      if (order.status !== lastStatus) {
        lastStatus = order.status;
        callback(order);
      }

      // Dừng polling nếu đạt trạng thái cuối
      if (['completed', 'cancelled'].includes(order.status)) {
        return;
      }

      // Tiếp tục polling
      setTimeout(poll, 5000);
    } catch (error) {
      console.error('Lỗi polling:', error);
      setTimeout(poll, 30000); // Tạm dừng khi có lỗi
    }
  };

  poll();
}

// Cách sử dụng
pollOrderStatus('order-123', (order) => {
  console.log(`Trạng thái đơn hàng: ${order.status}`);
});

Smart Polling (Tạm dừng lũy thừa)

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

      // Gọi callback nếu dữ liệu thay đổi
      if (JSON.stringify(data) !== JSON.stringify(lastData)) {
        lastData = data;
        callback(data);
      }

      // Dừng nếu điều kiện được đáp ứng
      if (stopCondition(data)) {
        return;
      }

      // Đặt lại khoảng thời gian nếu yêu cầu thành công
      interval = initialInterval;

    } catch (error) {
      retries++;
      if (retries >= maxRetries) {
        throw new Error('Đã vượt quá số lần thử lại tối đa');
      }
    }

    // Lên lịch poll tiếp theo với tạm dừng lũy thừa
    setTimeout(poll, interval);
    interval = Math.min(interval * 2, maxInterval);
  };

  poll();
}

// Cách sử dụng: Poll đơn hàng cho đến khi hoàn thành
smartPoll('https://petstoreapi.com/api/v1/orders/123',
  (order) => console.log('Đơn hàng:', order),
  {
    stopCondition: (order) => ['completed', 'cancelled'].includes(order.status),
    initialInterval: 2000,
    maxInterval: 30000
  }
);

Polling với 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) {
      // Chưa được sửa đổi, tiếp tục polling
      setTimeout(poll, 30000);
      return;
    }

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

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

  poll();
}

Triển khai Webhooks

Đăng ký Webhooks

// Đăng ký 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');
}

Nhận Webhooks

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

// Bộ phân tích cú pháp thân thô để xác minh chữ ký
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;

  // Xác minh chữ ký
  const isValid = verifySignature(body, signature, process.env.WEBHOOK_SECRET);

  if (!isValid) {
    return res.status(401).json({ error: 'Chữ ký không hợp lệ' });
  }

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

  // Xử lý sự kiện
  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;
  }

  // Xác nhận đã nhận
  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)
  );
}

Kiểm thử Webhooks tại cục bộ

# Sử dụng ngrok để công khai endpoint cục bộ
ngrok http 3000

# Đăng ký URL ngrok làm 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"]
  }'

Phân phối Webhook đáng tin cậy

Webhooks có thể thất bại. Triển khai logic thử lại.

Bên gửi (Máy chủ)

// Hàng đợi webhooks để phân phối
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);
      // Xóa khỏi hàng đợi khi thành công
      webhookQueue.splice(webhookQueue.indexOf(item), 1);
    } catch (error) {
      // Lên lịch thử lại với tạm dừng lũy thừa
      item.attempts++;
      item.nextAttempt = now + getBackoff(item.attempts);

      if (item.attempts >= 5) {
        // Đánh dấu là thất bại sau 5 lần thử
        await markWebhookFailed(item);
        webhookQueue.splice(webhookQueue.indexOf(item), 1);
      }
    }
  }

  setTimeout(processQueue, 5000);
}

function getBackoff(attempt) {
  // 1phút, 5phút, 15phút, 1giờ, 4giờ
  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}`);
  }
}

Bên nhận (Client)

// Xử lý webhook bất biến
const processedEvents = new Set();

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

  // Bỏ qua nếu đã được xử lý (tính bất biến)
  if (processedEvents.has(event.id)) {
    return res.status(200).json({ received: true });
  }

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

    // Xóa các ID sự kiện cũ (giữ lại 1000 ID cuối)
    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('Lỗi xử lý webhook:', error);
    // Trả về 5xx để kích hoạt thử lại
    res.status(500).json({ error: 'Xử lý thất bại' });
  }
});

async function processEvent(event) {
  // Xử lý sự kiện
  switch (event.type) {
    case 'order.created':
      await handleOrderCreated(event.data);
      break;
    // ... xử lý các sự kiện khác
  }
}

Phương pháp tiếp cận lai

Sử dụng cả polling và webhooks cho các cập nhật quan trọng.

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

  async start() {
    // Bắt đầu với polling để có phản hồi tức thì
    this.startPolling();

    // Đăng ký webhook để cập nhật thời gian thực
    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 // Tự động xóa sau lần gửi đầu tiên
      })
    });

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

Câu hỏi thường gặp

Hỏi: Tôi nên poll thường xuyên đến mức nào?Tùy thuộc vào mức độ khẩn cấp. 30 giây cho gần thời gian thực. 5 phút cho không khẩn cấp. Cân bằng độ mới với tải máy chủ.

Hỏi: Điều gì xảy ra nếu endpoint webhook của tôi bị sập?Các nhà cung cấp webhook tốt sẽ thử lại với tạm dừng lũy thừa. Triển khai tính bất biến để xử lý các lần gửi trùng lặp.

Hỏi: Làm cách nào để bảo mật webhooks?Xác minh chữ ký bằng cách sử dụng các khóa bí mật được chia sẻ. Chỉ sử dụng HTTPS. Xác thực dữ liệu sự kiện.

Hỏi: Tôi có thể sử dụng webhooks cho dữ liệu lịch sử không?Không. Webhooks chỉ dành cho các sự kiện mới. Sử dụng polling hoặc API theo lô cho dữ liệu lịch sử.

Hỏi: Tôi nên sử dụng polling hay webhooks cho ứng dụng di động?Polling đơn giản hơn cho di động. Webhooks yêu cầu thông báo đẩy làm trung gian.

Hỏi: Làm cách nào để gỡ lỗi các vấn đề về webhook?Sử dụng các công cụ như webhook.site để kiểm thử. Ghi nhật ký tất cả các lần gửi webhook. Cung cấp lịch sử sự kiện webhook trong API của bạn.

Modern PetstoreAPI hỗ trợ cả polling và webhooks. Xem hướng dẫn về webhooks để biết chi tiết triển khai. Kiểm thử tích hợp webhook với Apidog.

Thực hành thiết kế API trong Apidog

Khám phá cách dễ dàng hơn để xây dựng và sử dụng API