DEV Community

Cover image for Webhooks vs Polling: Pola Integrasi API Mana yang Lebih Baik?
Walse
Walse

Posted on • Originally published at apidog.com

Webhooks vs Polling: Pola Integrasi API Mana yang Lebih Baik?

TL;DR: Polling memeriksa pembaruan secara berkala (mudah tapi tidak efisien). Webhooks mengirim pembaruan secara real-time (efisien tapi lebih kompleks). Pakai polling untuk pengecekan jarang, webhooks untuk update real-time. Modern PetstoreAPI mendukung keduanya dengan pengiriman webhook yang andal.

Coba Apidog hari ini

Memahami Perbedaannya

Polling: Klien bertanya "Ada pembaruan?" secara berkala.
Webhooks: Server memberitahu "Ini update-nya!" saat ada perubahan.

  • Polling = Cek kotak surat tiap jam
  • Webhooks = Tukang pos membunyikan bel saat surat datang

Polling: Cara Kerjanya

Klien melakukan permintaan berkala untuk cek perubahan status.

// Poll setiap 30 detik
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

Pola polling:

Polling sederhana:

GET /api/v1/orders/123
# Mengembalikan status pesanan saat ini
Enter fullscreen mode Exit fullscreen mode

Polling kondisional (ETag):

GET /api/v1/orders/123
If-None-Match: "abc123"
# 304 jika tidak berubah, 200 jika berubah
Enter fullscreen mode Exit fullscreen mode

Polling berbasis sejak (since-based):

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

Webhooks: Cara Kerjanya

Server mengirim HTTP POST ke endpoint Anda saat event terjadi.

Alur pengaturan:

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

// 2. Server mengirim webhook saat event
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. Verifikasi dan proses webhook, respon 200 OK
Enter fullscreen mode Exit fullscreen mode

Kapan Menggunakan Polling

Gunakan polling untuk:

  • Pemeriksaan tidak sering (misal 1 jam sekali)
  • Resource sedikit
  • Implementasi sederhana
  • Kontrol penuh pada sisi klien
  • Testing & debugging

Contoh:

  • Cek status laporan harian
  • Sync kontak tiap beberapa menit
  • Monitor health server
  • Cek status pembayaran jarang-jarang

Polling tepat saat:

  • Update jarang
  • Delay bisa diterima
  • Butuh kode sederhana
  • Resource terbatas

Kapan Menggunakan Webhooks

Gunakan webhooks untuk:

  • Update real-time
  • Memantau banyak resource
  • Event sensitif waktu
  • Integrasi pihak ketiga
  • Update frekuensi tinggi

Contoh:

  • Konfirmasi pembayaran
  • Pesan chat
  • Alert harga saham
  • Perubahan status order
  • Notifikasi CI/CD

Webhooks optimal saat:

  • Update harus cepat
  • Polling tidak efisien
  • Banyak klien memantau resource sama
  • Ingin kurangi beban server

Tabel Perbandingan

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



















































Faktor Polling Webhooks
Latensi Hingga interval polling Real-time
Beban server Tinggi (banyak permintaan kosong) Rendah (hanya peristiwa nyata)
Kompleksitas Sederhana Kompleks
Keandalan Tinggi (klien mengontrol retry) Menengah (butuh retry logic)
Pengaturan Tidak ada Pendaftaran endpoint
Masalah Firewall Tidak ada (hanya keluar) Mungkin perlu whitelist
Biaya Lebih tinggi (lebih banyak request) Lebih rendah (sedikit request)
Terbaik untuk Pengecekan jarang Pembaruan real-time
<!--kg-card-end: html-->

Mengimplementasikan Polling

Polling Dasar

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

      // Callback hanya jika status berubah
      if (order.status !== lastStatus) {
        lastStatus = order.status;
        callback(order);
      }

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

      setTimeout(poll, 5000);
    } catch (error) {
      console.error('Polling error:', error);
      setTimeout(poll, 30000); // Retry dengan delay saat error
    }
  };

  poll();
}

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

Polling Cerdas (Exponential Backoff)

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 jika data berubah
      if (JSON.stringify(data) !== JSON.stringify(lastData)) {
        lastData = data;
        callback(data);
      }

      // Stop jika kondisi terpenuhi
      if (stopCondition(data)) {
        return;
      }

      // Reset interval jika sukses
      interval = initialInterval;

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

    setTimeout(poll, interval);
    interval = Math.min(interval * 2, maxInterval);
  };

  poll();
}

// Contoh penggunaan: Poll status order hingga selesai
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

Polling dengan 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) {
      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

Mengimplementasikan Webhooks

Mendaftarkan Webhooks

// Daftarkan endpoint webhook
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

Menerima Webhooks

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

// Parser body mentah untuk verifikasi signature
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;

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

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

  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

Menguji Webhooks Secara Lokal

# Ekspos endpoint lokal dengan ngrok
ngrok http 3000

# Daftar URL ngrok sebagai endpoint webhook
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

Pengiriman Webhook yang Andal

Webhooks bisa gagal, implementasikan retry logic.

Sisi Pengirim (Server)

// Antrean webhook untuk pengiriman
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);
      webhookQueue.splice(webhookQueue.indexOf(item), 1);
    } catch (error) {
      // Exponential backoff
      item.attempts++;
      item.nextAttempt = now + getBackoff(item.attempts);

      if (item.attempts >= 5) {
        // Tandai gagal setelah 5x
        await markWebhookFailed(item);
        webhookQueue.splice(webhookQueue.indexOf(item), 1);
      }
    }
  }

  setTimeout(processQueue, 5000);
}

function getBackoff(attempt) {
  // 1m, 5m, 15m, 1h, 4h
  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

Sisi Penerima (Klien)

// Penanganan webhook idempotent
const processedEvents = new Set();

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

  // Skip jika sudah diproses (idempotensi)
  if (processedEvents.has(event.id)) {
    return res.status(200).json({ received: true });
  }

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

    // Bersihkan ID event lama (1000 terakhir)
    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);
    // Kembali 5xx untuk trigger retry
    res.status(500).json({ error: 'Processing failed' });
  }
});

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

Pendekatan Hibrida

Gabungkan polling dan webhooks untuk update penting.

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

  async start() {
    // Start polling untuk feedback instan
    this.startPolling();

    // Daftarkan webhook untuk update real-time
    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 // Dihapus otomatis setelah pengiriman pertama
      })
    });

    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

FAQ

T: Seberapa sering polling? Tergantung kebutuhan. Untuk real-time, 30 detik. Tidak mendesak, 5 menit. Sesuaikan balance antara freshness dan beban server.

T: Bagaimana jika endpoint webhook down? Provider webhook yang baik akan retry dengan exponential backoff. Terapkan idempotensi (cek event.id) untuk handle duplikat.

T: Cara mengamankan webhooks? Verifikasi signature dengan shared secret, selalu gunakan HTTPS, dan validasi payload event.

T: Bisakah webhooks untuk data historis? Tidak. Webhooks hanya untuk event baru. Ambil data historis dengan polling atau batch API.

T: Polling atau webhooks untuk mobile app? Polling lebih simpel di mobile. Webhooks perlu push notification sebagai perantara.

T: Cara debug masalah webhook? Pakai tools seperti webhook.site untuk testing. Log semua pengiriman webhook dan sediakan riwayat event di API Anda.

Modern PetstoreAPI mendukung polling & webhooks. Lihat panduan webhooks untuk detail implementasi. Uji integrasi webhook dengan Apidog.

Top comments (0)