Webhook vs Polling vs SSE vs WebSocket: Real-Time Communication Compared
Every application eventually needs real-time (or near-real-time) data. The question is not if but how. Each approach has sharp tradeoffs in latency, complexity, scalability, and reliability.
Short Polling#
The simplest approach: ask the server repeatedly on a fixed interval.
// Client polls every 5 seconds
setInterval(async () => {
const response = await fetch('/api/notifications');
const data = await response.json();
updateUI(data);
}, 5000);
How it works: Client sends HTTP request, server responds immediately with current state, connection closes.
Pros: Dead simple, works everywhere, stateless server.
Cons: High latency (up to one full interval), wastes bandwidth when nothing changed, hammers the server.
Best for: Low-frequency updates where latency does not matter (checking deployment status every 30 seconds).
Long Polling#
The server holds the connection open until new data is available:
async function longPoll() {
try {
// Server holds this request until data is ready (or timeout)
const response = await fetch('/api/notifications/subscribe', {
signal: AbortSignal.timeout(30000)
});
const data = await response.json();
updateUI(data);
} catch (e) {
// Timeout or error — reconnect
}
// Immediately reconnect
longPoll();
}
longPoll();
How it works: Client sends request, server waits until there is new data (or a timeout), responds, client immediately reconnects.
Pros: Near-real-time latency, no wasted requests, works through proxies and firewalls.
Cons: Server holds many open connections, reconnection overhead, message ordering is tricky.
Best for: Chat applications before WebSocket was widely supported (the original Facebook chat used this).
Server-Sent Events (SSE)#
A one-way persistent stream from server to client over plain HTTP:
// Client
const source = new EventSource('/api/stream');
source.onmessage = (event) => {
const data = JSON.parse(event.data);
updateUI(data);
};
source.onerror = () => {
// Browser automatically reconnects with Last-Event-ID
console.log('Connection lost, reconnecting...');
};
// Server (Node.js)
app.get('/api/stream', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
const sendEvent = (data, id) => {
res.write(`id: ${id}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
// Subscribe to updates
const unsubscribe = eventBus.on('notification', (data) => {
sendEvent(data, data.id);
});
req.on('close', () => {
unsubscribe();
});
});
How it works: Single HTTP connection stays open. Server pushes events. Browser handles reconnection automatically using Last-Event-ID.
Pros: Built-in reconnection, event IDs for resumption, works over HTTP/2 (multiplexed), simple protocol.
Cons: Unidirectional (server to client only), limited to ~6 connections per domain in HTTP/1.1, text-only.
Best for: Live dashboards, stock tickers, notification feeds, build logs.
WebSocket#
Full-duplex, bidirectional communication over a single TCP connection:
// Client
const ws = new WebSocket('wss://api.example.com/ws');
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'subscribe', channel: 'trades' }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleMessage(data);
};
ws.onclose = () => {
// Must implement reconnection manually
setTimeout(reconnect, 1000);
};
// Server (Node.js with ws)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', (message) => {
const parsed = JSON.parse(message);
if (parsed.type === 'subscribe') {
channels.get(parsed.channel).add(ws);
}
});
ws.on('close', () => {
channels.forEach(set => set.delete(ws));
});
});
How it works: HTTP upgrade handshake, then persistent TCP connection. Both sides can send messages at any time.
Pros: True bidirectional, lowest latency, binary support, no HTTP overhead per message.
Cons: Stateful connections (harder to scale), no automatic reconnection, needs sticky sessions or pub/sub layer, some proxies/firewalls block it.
Best for: Multiplayer games, collaborative editing, trading platforms, real-time chat.
Webhooks#
Server-to-server push notifications via HTTP POST:
// Webhook sender (e.g., Stripe)
await fetch(customer.webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Signature': hmacSign(payload, secret)
},
body: JSON.stringify({
event: 'payment.completed',
data: { paymentId: 'pay_123', amount: 2999 }
})
});
// Webhook receiver (your server)
app.post('/webhooks/stripe', (req, res) => {
// Verify signature first
if (!verifySignature(req)) {
return res.status(401).end();
}
// Acknowledge immediately
res.status(200).json({ received: true });
// Process asynchronously
queue.add('process-webhook', req.body);
});
How it works: When an event occurs, the source system sends an HTTP POST to your registered URL.
Pros: No polling, real-time, efficient, decoupled systems.
Cons: Requires public endpoint, delivery not guaranteed (needs retry logic), ordering not guaranteed, debugging is harder.
Best for: Payment notifications, CI/CD triggers, third-party integrations, event-driven architectures.
Comparison Matrix#
| Criteria | Short Poll | Long Poll | SSE | WebSocket | Webhook |
|---|---|---|---|---|---|
| Latency | High | Low | Low | Lowest | Low |
| Direction | Client pull | Client pull | Server push | Bidirectional | Server push |
| Complexity | Trivial | Low | Low | Medium | Medium |
| Scalability | Poor | Fair | Good | Fair | Good |
| Connection cost | None | 1 per client | 1 per client | 1 per client | None |
| Auto-reconnect | N/A | Manual | Built-in | Manual | N/A |
| Binary support | Yes | Yes | No | Yes | Yes |
| Firewall-friendly | Yes | Yes | Yes | Sometimes | Yes |
| Browser support | All | All | All modern | All modern | N/A |
When to Use What#
Use short polling when: Simplicity matters more than efficiency, updates are infrequent, or you need a quick prototype.
Use long polling when: You need near-real-time but cannot use SSE/WebSocket (legacy proxy constraints).
Use SSE when: Server-to-client streaming is sufficient — dashboards, feeds, logs. It is simpler than WebSocket and HTTP/2 makes it scale well.
Use WebSocket when: You need true bidirectional communication — chat, gaming, collaborative editing.
Use webhooks when: The communication is server-to-server and event-driven — payment callbacks, CI triggers, third-party integrations.
Hybrid Approaches#
Real systems often combine multiple patterns:
Webhook + Polling fallback: Register a webhook for instant delivery, but poll periodically as a safety net for missed events.
Primary: Stripe → webhook → your server (instant)
Fallback: Cron job → GET /api/charges?since=last_check (every 5 min)
WebSocket + REST fallback: Use WebSocket for real-time updates, fall back to REST polling if the connection drops.
SSE + POST requests: SSE for server-to-client streaming, regular POST/PUT for client-to-server actions. This covers most use cases without WebSocket complexity.
Webhook + SSE to browser: Receive webhook server-side, fan out to browsers via SSE.
Stripe → webhook → your server → SSE → browser
This is often simpler than exposing WebSocket infrastructure.
Scaling Considerations#
- SSE/WebSocket: Require sticky sessions or a pub/sub layer (Redis, NATS) to broadcast across server instances.
- Webhooks: Scale horizontally with ease — each request is independent.
- Polling: Scales poorly because every client generates load regardless of changes. Use caching aggressively.
- Connection limits: A single server can handle 10K-100K concurrent SSE/WebSocket connections with proper tuning. Plan capacity accordingly.
Choosing the right real-time pattern is a system design decision, not a technology decision. Start with the simplest approach that meets your latency requirements, and add complexity only when you have evidence it is needed.
Article #273 of the Codelit engineering series. Browse all articles at codelit.io
Try these templates
Uber Real-Time Location System
Handles 5M+ GPS pings per second using H3 hexagonal geospatial indexing.
6 componentsReal-Time Collaborative Editor
Notion-like document editor with real-time collaboration, conflict resolution, and rich media.
9 componentsDiscord Voice & Communication Platform
Handles millions of concurrent voice calls with WebRTC, media servers, and guild-based routing.
10 componentsContinue learning
Server-Sent Events: The Complete Guide to SSE, EventSource & Real-Time Streaming
7 min readWebSocketWebSocket & Real-Time Architecture: Patterns for Chat, Gaming, and Live Data
6 min readAPI designLong-Running API Operations: Async Patterns, Polling, Webhooks, and the Google LRO Pattern
6 min readGo deeper on API design
As an Amazon Associate I earn from qualifying purchases. Codelit may receive a commission at no extra cost to you.
API Design Patterns
JJ Geewax · 2021
Chapter-length treatment of pagination, long-running operations and versioning, with trade-offs.
The Design of Web APIs, Second Edition
Arnaud Lauret · 2024
Walks one API from user needs to a finished OpenAPI spec, rejected drafts and rewrites included.
Principles of Web API Design
James Higginbotham · 2021
A job-story-to-endpoint process you can run with product people, not just a list of REST rules.
4.5 (55)Kindle editionDatabase Internals
Alex Petrov · 2019
Explains B-trees, LSM trees and consensus protocols so you can predict how a datastore will behave.
4.6 (560)#2 in Management Information Systems