DEV Community

Cover image for Webhooks vs Polling: Which API Integration Pattern Is Better?
Wanda
Wanda

Posted on • Originally published at apidog.com

Webhooks vs Polling: Which API Integration Pattern Is Better?

TL;DR:

Polling checks for updates periodically (simple but inefficient). Webhooks push updates in real-time (efficient but complex). Use polling for infrequent checks, webhooks for real-time updates. Modern PetstoreAPI supports both patterns with reliable webhook delivery.

Try Apidog today

Understanding the Difference

  • Polling: Client repeatedly asks, β€œAny updates?”
  • Webhooks: Server pushes updates to the client as soon as something happens.

Analogy:

  • Polling = Checking your mailbox every hour
  • Webhooks = Mail carrier rings your doorbell when mail arrives

Polling: How It Works

The client sends periodic requests to check for changes.

// Poll every 30 seconds
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

Polling patterns:

Simple polling:

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

Conditional polling (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-based polling:

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

Webhooks: How They Work

The server sends an HTTP POST to your endpoint when events occur.

Setup flow:

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

// 2. Server sends webhook when event occurs
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. Verify and process webhook
// Respond with 200 OK
Enter fullscreen mode Exit fullscreen mode

When to Use Polling

Good for:

  • Infrequent checks (e.g., once per hour)
  • Small number of resources
  • Simple implementations
  • When you control the client
  • Testing and debugging

Examples:

  • Checking daily report status
  • Syncing contacts every few minutes
  • Monitoring server health
  • Checking payment status (infrequent)

Use polling when:

  • Updates are rare
  • Slight delay is acceptable
  • You want simple implementation
  • Resource is small

When to Use Webhooks

Good for:

  • Real-time updates
  • Monitoring many resources
  • Time-sensitive events
  • Third-party integrations
  • High-frequency updates

Examples:

  • Payment confirmations
  • Chat messages
  • Stock price alerts
  • Order status changes
  • CI/CD notifications

Use webhooks when:

  • Updates need to be immediate
  • Polling would be inefficient
  • Many clients monitor the same resource
  • You want to reduce server load

Comparison Table

Factor Polling Webhooks
Latency Up to poll interval Real-time
Server load High (many empty requests) Low (only real events)
Complexity Simple Complex
Reliability High (client controls retry) Medium (need retry logic)
Setup None Endpoint registration
Firewall issues None (outbound only) May need whitelisting
Cost Higher (more requests) Lower (fewer requests)
Best for Infrequent checks Real-time updates

Implementing Polling

Basic Polling

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

Smart Polling (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 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

Polling with 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

Implementing Webhooks

Registering Webhooks

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

Receiving Webhooks

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

Testing Webhooks Locally

# 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

Reliable Webhook Delivery

Webhooks can fail. Implement retry logic for reliability.

Sender Side (Server)

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

Receiver Side (Client)

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

Hybrid Approach

Combine polling and webhooks for critical updates.

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

FAQ

Q: How often should I poll?

Depends on urgency. Use 30 seconds for near-real-time, 5 minutes for non-urgent. Balance freshness with server load.

Q: What if my webhook endpoint is down?

Good webhook providers retry with exponential backoff. Implement idempotency to handle duplicate deliveries.

Q: How do I secure webhooks?

Verify signatures using shared secrets. Use HTTPS only. Validate event data.

Q: Can I use webhooks for historical data?

No. Webhooks are for new events only. Use polling or batch APIs for historical data.

Q: Should I use polling or webhooks for mobile apps?

Polling is simpler for mobile. Webhooks require push notifications as intermediary.

Q: How do I debug webhook issues?

Use tools like webhook.site for testing. Log all webhook deliveries. Provide webhook event history in your API.

Modern PetstoreAPI supports both polling and webhooks. See the webhooks guide for implementation details. Test webhook integrations with Apidog.

Top comments (0)