Webhooks vs Polling: Which API Integration Pattern Is Better?

Polling periodically checks an API for changes, while webhooks push events to you in real time. Learn when to use simple, client-controlled polling versus event-driven webhooks, see concrete code examples, and discover hybrid patterns so your integrations stay responsive without wasting requests.

Ashley Innocent

Ashley Innocent

20 March 2026

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.

Understanding the Difference

Polling: Client asks “Any updates?” repeatedly. Webhooks: Server says “Here’s an update!” when something happens.

Analogy:

Polling: How It Works

Client makes 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);

Polling patterns:

Simple polling:

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

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

Since-based polling:

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

Webhooks: How They Work

Server sends 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

When to Use Polling

Good for:

Examples:

Polling is fine when:

When to Use Webhooks

Good for:

Examples:

Webhooks are better when:

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

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

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

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

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

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"]
  }'

Reliable Webhook Delivery

Webhooks can fail. Implement retry logic.

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

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

Hybrid Approach

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

FAQ

Q: How often should I poll?Depends on urgency. 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.

Explore more

What Is MiroFish? A Multi-Agent AI Simulation Platform for Predicting Social Media Outcomes

What Is MiroFish? A Multi-Agent AI Simulation Platform for Predicting Social Media Outcomes

New to multi-agent simulation? Learn what MiroFish is, how it creates digital parallel worlds with AI agents, and why researchers use it for social media prediction.

19 March 2026

What Is The Agency Agents?

What Is The Agency Agents?

The Agency is 147 specialized AI agents distributed across 12 divisions. Each agent has personality, deliverables, and success metrics. This technical deep dive covers agent architecture, multi-tool integration (Claude Code, Cursor, Aider, Windsurf), MCP memory, and the bash s...

19 March 2026

Can't Afford Postman? The Ultimate Postman Alternative for API Testing

Can't Afford Postman? The Ultimate Postman Alternative for API Testing

With the massive March 2026 pricing update, Postman completely restricted its free plan to a single user, leaving development teams searching for a suitable Postman replacement. Explore our comprehensive Postman vs Apidog breakdown to see why Apidog is the best Postman alternative available.

16 March 2026

Practice API Design-first in Apidog

Discover an easier way to build and use APIs