Webhook vs Polling vs SSE vs WebSocket: Real-Time Communication Compared
Webhook vs Polling vs SSE vs WebSocket#
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 it on Codelit
GitHub Integration
Paste any repo URL to generate an interactive architecture diagram from real code
Related articles
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 componentsBuild this architecture
Generate an interactive architecture for Webhook vs Polling vs SSE vs WebSocket in seconds.
Try it in Codelit →
Comments