DEV Community

Preecha
Preecha

Posted on

Webhooks vs Polling: Which API Integration Pattern Is Better?

TL;DR: Polling checks for updates on a schedule—simple to implement, but potentially inefficient. Webhooks push updates when events happen—efficient and near-real-time, but require endpoint and retry handling. Use polling for infrequent checks and webhooks for time-sensitive updates. Modern PetstoreAPI supports both patterns with reliable webhook delivery.

Try Apidog today

Polling vs. Webhooks

Polling means the client repeatedly asks: “Has anything changed?”

Webhooks mean the server sends a request to your application when something changes.

  • Polling: checking your mailbox every hour
  • Webhooks: the mail carrier rings your doorbell when mail arrives

Choose based on how quickly you need updates, how many resources you monitor, and how much infrastructure you can manage.

How Polling Works

With polling, the client makes periodic requests for the latest resource state.

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

Common polling patterns

Simple polling

Request the current resource state on a fixed interval.

GET /api/v1/orders/123
Enter fullscreen mode Exit fullscreen mode

The API returns the current order state.

Conditional polling with ETags

Send the last ETag value using If-None-Match.

GET /api/v1/orders/123
If-None-Match: "abc123"
Enter fullscreen mode Exit fullscreen mode

The server can return:

  • 304 Not Modified when nothing changed
  • 200 OK with updated data when the resource changed

This reduces response payloads when updates are rare.

Since-based polling

Request only events created after a timestamp.

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

This is useful when you need an event stream rather than the latest resource snapshot.

How Webhooks Work

With webhooks, the server sends an HTTP POST request to your endpoint when a subscribed event occurs.

A typical flow looks like this:

POST /api/v1/webhooks
Content-Type: application/json

{
  "url": "https://myapp.com/webhooks/petstore",
  "events": ["order.created", "order.completed"],
  "secret": "whsec_abc123"
}
Enter fullscreen mode Exit fullscreen mode

When an order completes, the server sends an event to your endpoint:

POST https://myapp.com/webhooks/petstore
Content-Type: application/json

{
  "id": "evt_123",
  "type": "order.completed",
  "created": 1710331200,
  "data": {
    "orderId": "123",
    "status": "completed",
    "completedAt": "2024-01-01T12:00:00Z"
  }
}
Enter fullscreen mode Exit fullscreen mode

Your application verifies the request, processes the event, and responds with 200 OK.

When to Use Polling

Polling is a practical choice when you need a simple client-side implementation.

Use it for:

  • Infrequent checks, such as once per hour
  • A small number of resources
  • Testing and debugging
  • Clients you fully control
  • Cases where a short delay is acceptable

Typical examples:

  • Checking daily report status
  • Syncing contacts every few minutes
  • Monitoring server health
  • Checking a payment status infrequently

Polling is usually sufficient when updates are rare and implementation simplicity matters more than immediate delivery.

When to Use Webhooks

Webhooks are a better fit when your application needs updates as events happen.

Use them for:

  • Real-time or near-real-time updates
  • High-frequency events
  • Large numbers of resources
  • Third-party integrations
  • Time-sensitive workflows

Typical examples:

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

Webhooks reduce unnecessary requests because the server sends notifications only when an event occurs.

Comparison

Factor Polling Webhooks
Latency Up to the polling interval Real-time
Server load High when many requests return no changes Low because only real events are sent
Complexity Simple More complex
Reliability High because the client controls retries Medium because delivery retries are required
Setup No endpoint registration Requires endpoint registration
Firewall issues None; outbound requests only May require endpoint whitelisting
Cost Higher due to more requests Lower due to fewer requests
Best for Infrequent checks Real-time updates

Implement Polling

Basic polling with a terminal state

This example polls an order, triggers a callback only when the status changes, and stops after the order reaches a terminal state.

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 notify when the status changes.
      if (order.status !== lastStatus) {
        lastStatus = order.status;
        callback(order);
      }

      // Stop polling when the order is final.
      if (['completed', 'cancelled'].includes(order.status)) {
        return;
      }

      setTimeout(poll, 5000);
    } catch (error) {
      console.error('Polling error:', error);

      // Back off after an error.
      setTimeout(poll, 30000);
    }
  };

  poll();
}

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

Smart polling with exponential backoff

For longer-running operations, increase the polling interval over time. This reduces request volume while the operation is still pending.

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

      // Notify only when the response changes.
      if (JSON.stringify(data) !== JSON.stringify(lastData)) {
        lastData = data;
        callback(data);
      }

      // Stop once the caller-defined condition is met.
      if (stopCondition(data)) {
        return;
      }

      // Reset the interval after a successful request.
      interval = initialInterval;
    } catch (error) {
      retries++;

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

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

  poll();
}

// Usage: poll until the order reaches a terminal state.
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 ETags

Use ETags when the API supports them to avoid downloading unchanged data.

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) {
      // Nothing changed.
      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

Implement Webhooks

Register a webhook endpoint

Register your publicly accessible endpoint and the events it should receive.

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

Store the generated secret securely. Your receiving endpoint uses it to verify that incoming events were sent by the expected provider.

Receive and verify webhook events

Use the raw request body for signature verification. Do not parse JSON before checking the signature.

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

const app = express();

// Use the raw request body 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;

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

  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

Test webhooks locally

Use ngrok to expose a local server to the public internet.

ngrok http 3000
Enter fullscreen mode Exit fullscreen mode

Then register the generated URL as your 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

Implement Reliable Webhook Delivery

Webhook delivery can fail because of network problems, server outages, or temporary application errors. Reliable implementations need retries on the sender side and idempotency on the receiver side.

Sender: queue and retry webhook deliveries

This sender-side example queues deliveries and retries failures with exponential backoff.

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 successful deliveries from the queue.
      webhookQueue.splice(webhookQueue.indexOf(item), 1);
    } catch (error) {
      item.attempts++;
      item.nextAttempt = now + getBackoff(item.attempts);

      if (item.attempts >= 5) {
        // Mark the delivery as failed after five attempts.
        await markWebhookFailed(item);
        webhookQueue.splice(webhookQueue.indexOf(item), 1);
      }
    }
  }

  setTimeout(processQueue, 5000);
}

function getBackoff(attempt) {
  // 1 minute, 5 minutes, 15 minutes, 1 hour, 4 hours
  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: process events idempotently

Providers may retry an event after a timeout or a 5xx response. Your receiver must safely handle duplicate event IDs.

const processedEvents = new Set();

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

  // Ignore events that were already processed.
  if (processedEvents.has(event.id)) {
    return res.status(200).json({ received: true });
  }

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

    // Keep only the latest 1,000 event IDs.
    if (processedEvents.size > 1000) {
      const eventIds = Array.from(processedEvents);

      eventIds
        .slice(0, eventIds.length - 1000)
        .forEach((id) => processedEvents.delete(id));
    }

    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook processing error:', error);

    // A 5xx response tells the sender to retry.
    res.status(500).json({ error: 'Processing failed' });
  }
});

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

Use a Hybrid Approach for Critical Updates

For critical workflows, use polling and webhooks together:

  1. Start polling to provide immediate status checks.
  2. Register a webhook for real-time delivery.
  3. Stop polling when the resource reaches a terminal state.
  4. Delete one-time webhook registrations when they are no longer needed.
class OrderMonitor {
  constructor(orderId, callback) {
    this.orderId = orderId;
    this.callback = callback;
    this.pollInterval = null;
  }

  async start() {
    // Start polling for immediate feedback.
    this.startPolling();

    // Register a webhook for real-time updates.
    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 // Automatically delete after the 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

How often should I poll?

Match the interval to the urgency of the update. Use around 30 seconds for near-real-time status checks and around 5 minutes for non-urgent updates. Balance freshness against server load.

What happens if my webhook endpoint is down?

Webhook providers can retry failed deliveries with exponential backoff. Your endpoint should be idempotent because retries can produce duplicate events.

How do I secure webhooks?

Verify signatures using a shared secret, use HTTPS, and validate event data before processing it.

Can I use webhooks for historical data?

No. Webhooks deliver new events only. Use polling or batch APIs to retrieve historical data.

Should I use polling or webhooks for mobile apps?

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

How do I debug webhook issues?

Use tools such as webhook.site for testing, log webhook deliveries, and provide webhook event history in your API.

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

Top comments (0)