Webhook vs Polling vs WebSocket: When to Use Each
Three patterns power most real-time data flows in modern applications: webhooks, polling, and WebSockets. Developers often pick one by habit or familiarity rather than by what the use case actually requires.
This guide explains the real differences, the trade-offs, and the concrete situations where each pattern wins.
The Core Problem
Your application needs to know when something changes in an external system. A payment succeeds. A new GitHub commit is pushed. A user sends a message. A sensor reading exceeds a threshold.
The question is: how does your code find out?
Polling: Your Code Asks
Polling is the simplest mental model. Your application asks the external system at regular intervals: "Has anything changed?"
Your Server External API
| |
|-- GET /payments?after=5m ----->|
|<---- [] (nothing new) ---------|
| |
| [5 minutes pass] |
| |
|-- GET /payments?after=5m ----->|
|<---- [{ id: "pay_123" }] ------|
| |
| [process the payment] |
When Polling Works Well
- The external system does not support webhooks — some older APIs are read-only
- You control the polling rate — if 1-minute latency is acceptable and the API rate limits are generous
- Simple operational requirements — polling is stateless; your process restarts without losing state
- Batch data synchronization — syncing data from a source system overnight, where latency does not matter
The Problems with Polling
Latency — You only discover changes at the next poll cycle. If you poll every 5 minutes, your average latency is 2.5 minutes.
Wasted work — Most poll requests return nothing. If your payment succeeds once per hour but you poll every minute, 98% of requests are empty.
Rate limits — Frequent polling burns your API quota. If the API limits you to 1000 requests per day, polling every 2 minutes uses 720 of them.
Scaling poorly — As you add more resources to track (more users, more accounts), the polling load scales linearly.
// Polling example (Node.js)
const POLL_INTERVAL = 5 * 60 * 1000; // 5 minutes
async function pollForNewPayments(lastCheckedAt) {
const response = await fetch(
`https://api.payment-provider.com/payments?created_after=${lastCheckedAt}`
);
const { payments } = await response.json();
for (const payment of payments) {
await processPayment(payment);
}
return Date.now();
}
// Simple polling loop
async function startPolling() {
let lastCheckedAt = Date.now();
while (true) {
lastCheckedAt = await pollForNewPayments(lastCheckedAt);
await sleep(POLL_INTERVAL);
}
}
Webhooks: The System Tells You
Webhooks flip the direction. Instead of asking repeatedly, you give the external system a URL and it calls you when something happens.
Your Server External API
| |
| [event occurs] |
| |
|<-- POST /webhooks/payment -----|
| { type: "payment.success" } |
|---- 200 OK ------------------->|
When Webhooks Win
- Event-driven workflows — fulfilling orders, sending confirmation emails, triggering provisioning
- Low-latency requirements — webhooks typically deliver within seconds of the event
- High-volume event sources — more efficient than polling for systems generating many events
- You have a public-facing server — webhooks require a URL the sender can reach
The Problems with Webhooks
You need a receiver — Your server must be publicly reachable with a stable HTTPS URL. Local development requires extra setup (HookCap, ngrok, Stripe CLI).
Delivery is not guaranteed — Most providers retry on failure, but eventually give up. You must design for missed events.
Out-of-order delivery — Events may arrive in a different order than they occurred. Your handler must be order-independent.
The sender controls the rate — You cannot control how fast events arrive. A burst of 1000 events can overwhelm a handler that assumed a steady drip.
// Webhook handler (Express)
app.post('/webhooks/payment', express.raw({ type: '*/*' }), async (req, res) => {
// Must verify signature before trusting payload
if (!verifySignature(req)) {
return res.status(401).send('Unauthorized');
}
const event = JSON.parse(req.body);
// Acknowledge immediately
res.json({ received: true });
// Process async
await queue.add('payment', { event });
});
WebSocket: A Persistent Two-Way Connection
WebSocket is a protocol, not just a pattern. It establishes a persistent connection between client and server where either side can push messages at any time.
Browser / Client Server
| |
|--- WebSocket Handshake ----->|
|<-- 101 Switching Protocols--|
| |
| [connection open] |
| |
|<-- { type: "message" } -----| (server pushes)
|<-- { type: "presence" } ----| (server pushes)
|--- { type: "ping" } ------->| (client sends)
|<-- { type: "pong" } --------|
| |
| [connection open indefinitely...]
When WebSockets Win
- User-facing real-time features — chat, live notifications, collaborative editing, live sports scores
- Bidirectional communication — both client and server need to initiate messages
- Sub-second latency requirements — WebSocket latency is typically under 100ms
- High message frequency — streaming sensor data, live price ticks, game state
The Problems with WebSockets
Connection management — You must handle disconnections, reconnections, and heartbeats
Stateful servers — Load balancers need sticky sessions or a pub/sub layer (Redis) to broadcast to all connected clients
Firewall and proxy issues — Some enterprise networks block non-HTTP protocols or WebSocket upgrades
Not suitable for server-to-server — WebSockets are primarily a browser protocol; server-to-server async events use webhooks instead
// WebSocket server (Node.js with ws)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws, req) => {
const userId = getUserFromRequest(req);
ws.on('message', (message) => {
const data = JSON.parse(message);
handleClientMessage(userId, data);
});
ws.on('close', () => {
cleanupUserConnection(userId);
});
// Push updates to this client
ws.send(JSON.stringify({ type: 'connected', userId }));
});
// Push to all connected clients
function broadcastUpdate(event) {
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(event));
}
});
}
Side-by-Side Comparison
| Polling | Webhook | WebSocket | |
|---|---|---|---|
| Who initiates | Your client | Remote server | Either side |
| Latency | High (poll interval) | Low (seconds) | Lowest (ms) |
| Connection | Stateless HTTP | Stateless HTTP | Persistent |
| Requires public URL | No | Yes | Server needs public URL |
| Suitable for browsers | Yes | No (servers only) | Yes |
| Delivery guarantee | You control retries | Provider retries | Application layer |
| Scales with event volume | Poorly | Well | Depends on infra |
| Operational complexity | Low | Medium | High |
Decision Guide
Use webhooks when:
- An external service needs to notify your server about events
- You are integrating with Stripe, GitHub, Shopify, Twilio, or any major platform
- Latency of a few seconds is acceptable
- Events are infrequent relative to polling cost
Use polling when:
- The data source does not support webhooks
- You are running batch jobs where latency does not matter
- You need a simple, stateless integration with no infrastructure to manage
Use WebSockets when:
- Users need to see updates in a browser in real time
- You need bidirectional communication (not just server-to-client)
- Sub-second latency matters
- You are building chat, collaborative tools, live dashboards, or multiplayer games
Hybrid Approaches
Real systems often combine patterns:
Webhook + WebSocket: An external event hits your server via webhook. Your server processes it and pushes a notification to connected browser clients via WebSocket.
Stripe --> Webhook --> Your Server --> WebSocket --> Browser
This is the standard pattern for live payment dashboards: Stripe tells your server a payment succeeded (webhook), your server tells the user's browser to update the UI (WebSocket).
Polling + Webhook fallback: Some systems poll as a safety net even when webhooks are configured. If a webhook was missed (delivery failure), the next poll will catch the event.
Long polling: A middle ground between polling and WebSockets. Your client makes an HTTP request, the server holds it open until something happens, then responds. The client immediately makes another request. Lower latency than regular polling, no WebSocket protocol complexity, but higher server resource usage than WebSocket.
Webhooks and HookCap
If you are debugging webhook integrations, HookCap gives you a persistent HTTPS endpoint to capture real webhook deliveries and replay them. You can:
- Inspect the exact payload a provider sends without exposing your local server
- Replay events to test handler changes without re-triggering events in the provider's system
- Use Auto-Forward (Pro) to proxy live webhooks to your local development server
HookCap uses WebSocket internally to push captured events to your dashboard in real time — a practical example of using webhooks and WebSockets together.
Summary
- Polling — simple, stateless, high latency, wasteful. Use when the data source does not push.
- Webhooks — event-driven, low latency, requires a public receiver. The right choice for server-to-server integrations with modern APIs.
- WebSocket — persistent, bidirectional, lowest latency. The right choice for browser real-time features.
Most production systems use all three. The key is matching the pattern to the problem, not defaulting to the one you know best.
Top comments (0)