Polling vs. webhooks vs. SSE vs. WebSockets
How to choose between polling, webhooks, Server-Sent Events, and WebSockets: latency, infrastructure complexity, directionality, scalability, and which pattern fits each use case.
TL;DR
| Pattern | Best for | Direction | Latency | Complexity |
|---|---|---|---|---|
| Polling | Simple read-heavy checks, low-frequency updates | Client β Server | Seconds to minutes | Low |
| Webhooks | Server-to-server event notifications | Server β Server | Near-instant | Medium |
| SSE | One-way live feeds to browser clients | Server β Client | Sub-second | Low-medium |
| WebSockets | Bidirectional real-time (chat, gaming, collaboration) | Bidirectional | Sub-100ms | High |
Default answer: SSE for server-to-client pushes. WebSockets only when the client needs to send data back. Webhooks for server-to-server. Polling only when you genuinely can't do better.
The Framing
Your team ships a dashboard that shows order status. Version one polls the API every 5 seconds. It works. Then the product manager wants real-time updates. Then Finance notices the API bill: 90% of those polling requests return "no change." You're burning bandwidth, database queries, and money on empty responses.
This is the core tension: who initiates the communication, and how often?
Polling puts the client in control. Simple, but wasteful. Push-based approaches (webhooks, SSE, WebSockets) put the server in control. More efficient, but more infrastructure. The right choice depends on directionality, latency requirements, and what your client actually is: browser, mobile app, or another server.
How Each Works
Polling: Client Pulls on a Timer
The simplest approach. The client sends HTTP requests at a fixed interval and checks for new data.
// Short polling: fixed interval
const poll = async () => {
while (true) {
const res = await fetch('/api/orders/123/status');
const data = await res.json();
updateUI(data);
await sleep(5000); // 5-second interval
}
};
// Long polling: server holds connection until data arrives
const longPoll = async () => {
while (true) {
// Server holds this request open up to 30 seconds
const res = await fetch('/api/orders/123/status?wait=30');
const data = await res.json();
updateUI(data);
// Immediately reconnect
}
};
Short polling fires requests at a fixed interval (1s, 5s, 30s). Most responses are empty. At 5-second intervals with 10,000 clients, that's 2,000 requests/second, even when nothing changes.
Long polling is smarter. The server holds the connection until new data arrives (or a 30-second timeout). This simulates push over HTTP, but each "event" still requires a full request/response cycle. Both work through any HTTP infrastructure with zero special configuration.
For your interview: if someone asks about real-time and you mention polling, immediately follow with "but polling wastes bandwidth, so I'd prefer SSE or WebSockets depending on the direction of data flow."
Webhooks: Server POSTs to Your URL
Webhooks flip the model: instead of the client asking for updates, the server sends an HTTP POST to a pre-registered URL when an event occurs.
// Registration: tell the provider where to send events
await stripe.webhookEndpoints.create({
url: 'https://api.myapp.com/webhooks/stripe',
enabled_events: ['payment_intent.succeeded', 'charge.failed'],
});
// Receiver: handle incoming events
app.post('/webhooks/stripe', (req, res) => {
// Step 1: Verify signature (CRITICAL)
const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(
req.body, sig, process.env.STRIPE_WEBHOOK_SECRET
);
// Step 2: Process idempotently (webhooks retry)
await processEvent(event.id, event.type, event.data);
// Step 3: Respond 200 quickly (provider times out at ~5-15s)
res.status(200).json({ received: true });
});
The webhook receiver must be a publicly-reachable server. This makes webhooks ideal for server-to-server integrations (Stripe, GitHub, Twilio) but useless for browser or mobile clients.
Delivery guarantees matter. Webhook providers retry failed deliveries (typically exponential backoff over 24-72 hours). Your receiver must be idempotent because you will receive the same event multiple times. Always deduplicate by event ID.
Security is non-negotiable. Every webhook payload must include an HMAC signature. Without verification, anyone can POST fake events to your endpoint. Stripe uses HMAC-SHA256 with a shared secret. GitHub uses the X-Hub-Signature-256 header.
SSE: Server Streams Events Over HTTP
Server-Sent Events use a persistent HTTP connection where the server pushes text-based event frames to the client. One direction only: server to client.
// Server (Node.js/Express)
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const send = (data: object) => {
res.write(`id: ${Date.now()}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
orderService.on('statusChange', send);
req.on('close', () => orderService.off('statusChange', send));
});
// Client (browser, native API, no library needed)
const source = new EventSource('/events');
source.onmessage = (e) => {
const data = JSON.parse(e.data);
updateUI(data);
};
// Auto-reconnects on disconnect with Last-Event-ID header
The EventSource browser API handles reconnection automatically. If the connection drops, the browser reconnects and sends the Last-Event-ID header so the server can replay missed events. Built into the spec, no library needed.
SSE works through standard HTTP infrastructure. With HTTP/2 multiplexing, multiple SSE streams share a single TCP connection, eliminating the old "6 connections per domain" browser limit.
My recommendation for most server-to-client real-time needs: SSE first, WebSockets only when you need client-to-server messaging.
WebSockets: Full Duplex Over a Persistent Connection
WebSockets upgrade an HTTP connection to a persistent, full-duplex protocol. After the handshake, both sides send frames freely with minimal overhead (2-14 bytes per frame vs. hundreds of bytes per HTTP request).
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.