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.
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);
Common polling patterns
Simple polling
Request the current resource state on a fixed interval.
GET /api/v1/orders/123
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"
The server can return:
-
304 Not Modifiedwhen nothing changed -
200 OKwith 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
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"
}
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"
}
}
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}`);
});
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
}
);
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();
}
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')}`;
}
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)
);
}
Test webhooks locally
Use ngrok to expose a local server to the public internet.
ngrok http 3000
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"]
}'
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}`);
}
}
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.
}
}
Use a Hybrid Approach for Critical Updates
For critical workflows, use polling and webhooks together:
- Start polling to provide immediate status checks.
- Register a webhook for real-time delivery.
- Stop polling when the resource reaches a terminal state.
- 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'
}
);
}
}
}
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)