WebSockets vs Server-Sent Events vs HTTP/2 Push: Real-time Communication Guide
Master real-time web communication by comparing WebSockets, Server-Sent Events, and HTTP/2 Server Push. Learn when to use each technology with practical examples and performance insights.
Table of Contents
- Introduction
- Understanding Real-time Communication
- WebSockets: Full-Duplex Communication
- Server-Sent Events: One-Way Server Push
- HTTP/2 Server Push: Resource Preloading
- Comparing the Technologies
- Performance Considerations
- Use Cases and When to Choose
- Implementation Examples
- Best Practices and Common Pitfalls
- Conclusion
Introduction
Real-time communication has become a fundamental requirement for modern web applications. Whether you’re building a chat application, live dashboard, collaborative editor, or real-time notifications system, choosing the right technology for bidirectional or unidirectional data flow can make or break your application’s performance and user experience.
The web offers several technologies for real-time communication, each with distinct characteristics, use cases, and trade-offs. WebSockets provide full-duplex communication channels, Server-Sent Events (SSE) enable efficient one-way server-to-client messaging, and HTTP/2 Server Push offers resource preloading capabilities. Understanding when and why to use each technology is crucial for building scalable, performant applications.
This comprehensive guide will help you understand the differences between WebSockets, Server-Sent Events, and HTTP/2 Server Push. You’ll learn how each technology works, when to use them, and how to implement them effectively. We’ll cover practical examples, performance considerations, and real-world patterns that will help you make informed architectural decisions.
By the end of this guide, you’ll be able to evaluate your application’s real-time communication needs and choose the most appropriate technology, whether you’re building a simple notification system or a complex multiplayer game.
Understanding Real-time Communication
Before diving into specific technologies, it’s essential to understand what real-time communication means in web applications and why traditional HTTP requests fall short.
The HTTP Request-Response Limitation
Traditional HTTP follows a request-response pattern: the client sends a request, waits for the server to respond, and the connection closes. This works well for static content and simple interactions, but it has significant limitations for real-time applications:
- No server-initiated communication: The server cannot push data to the client without an explicit request
- Overhead: Each request requires establishing a new connection (unless HTTP keep-alive is used)
- Latency: Polling (repeatedly requesting updates) introduces unnecessary delay and server load
- Inefficiency: Long polling (keeping requests open) consumes resources even when no data is available
Real-time Communication Requirements
Real-time applications typically need:
- Low latency: Data should reach clients as quickly as possible
- Efficient resource usage: Minimal overhead for maintaining connections
- Scalability: Ability to handle many concurrent connections
- Reliability: Automatic reconnection and error handling
- Bi-directional or uni-directional: Depending on use case, you may need two-way or one-way communication
The Evolution of Real-time Web Technologies
The web has evolved several solutions to address these needs:
- WebSockets: Full-duplex communication over a single TCP connection
- Server-Sent Events: One-way server-to-client streaming over HTTP
- HTTP/2 Server Push: Server-initiated resource delivery (though deprecated in HTTP/3)
Each technology addresses different use cases and has unique characteristics that make them suitable for specific scenarios.
WebSockets: Full-Duplex Communication
WebSockets provide a persistent, full-duplex communication channel between client and server. Once established, both parties can send messages at any time without the overhead of HTTP headers.
How WebSockets Work
WebSockets start with an HTTP handshake that upgrades the connection to the WebSocket protocol (ws:// or wss://). After the upgrade, the connection remains open, allowing bidirectional communication with minimal overhead.
Key Characteristics:
- Full-duplex: Both client and server can send messages simultaneously
- Persistent connection: Single TCP connection stays open
- Low overhead: After handshake, only data frames are sent (2-14 bytes overhead)
- Binary and text support: Can send both text and binary data
- Protocol-level: Operates at a lower level than HTTP
WebSocket Handshake Process
// Client-side WebSocket connectionconst ws = new WebSocket("wss://example.com/chat");
// Connection openedws.onopen = () => { console.log("WebSocket connection established"); // Send a message to the server ws.send(JSON.stringify({ type: "join", room: "general" }));};
// Receive messages from serverws.onmessage = (event) => { const data = JSON.parse(event.data); console.log("Received:", data);
// Handle different message types if (data.type === "message") { displayMessage(data.content); } else if (data.type === "userJoined") { updateUserList(data.users); }};
// Handle errorsws.onerror = (error) => { console.error("WebSocket error:", error);};
// Handle connection closews.onclose = (event) => { console.log("Connection closed:", event.code, event.reason); // Implement reconnection logic setTimeout(() => reconnect(), 1000);};
// Send message to serverfunction sendMessage(text) { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: "message", content: text })); }}Server-Side WebSocket Implementation
// Node.js with ws libraryconst WebSocket = require("ws");
const wss = new WebSocket.Server({ port: 8080 });
// Store connected clientsconst clients = new Map();
wss.on("connection", (ws, req) => { const clientId = generateClientId(); clients.set(clientId, ws);
console.log(`Client ${clientId} connected`);
// Send welcome message ws.send( JSON.stringify({ type: "welcome", clientId: clientId, timestamp: Date.now(), }), );
// Handle incoming messages ws.on("message", (message) => { try { const data = JSON.parse(message);
// Broadcast to all clients broadcast({ type: "message", from: clientId, content: data.content, timestamp: Date.now(), }); } catch (error) { ws.send( JSON.stringify({ type: "error", message: "Invalid message format", }), ); } });
// Handle disconnection ws.on("close", () => { clients.delete(clientId); broadcast({ type: "userLeft", clientId: clientId, }); console.log(`Client ${clientId} disconnected`); });
// Handle errors ws.on("error", (error) => { console.error(`WebSocket error for client ${clientId}:`, error); });});
function broadcast(data) { const message = JSON.stringify(data); clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(message); } });}WebSocket Advantages ✅
- Low latency: Minimal overhead after initial handshake
- Full-duplex: True bidirectional communication
- Efficient: Single persistent connection
- Binary support: Can send binary data efficiently
- Wide browser support: Available in all modern browsers
WebSocket Disadvantages ❌
- Complexity: Requires connection management, reconnection logic, and error handling
- Stateful: Server must maintain connection state
- Proxy issues: Some proxies and firewalls may block WebSocket connections
- No automatic reconnection: Must implement reconnection logic manually
- Resource intensive: Each connection consumes server resources
Server-Sent Events: One-Way Server Push
Server-Sent Events (SSE) provide a simple way for servers to push data to clients over HTTP. Unlike WebSockets, SSE is unidirectional—data flows only from server to client.
How Server-Sent Events Work
SSE uses standard HTTP connections with a special text/event-stream content type. The server keeps the connection open and sends data in a specific format. The browser’s EventSource API handles reconnection automatically.
Key Characteristics:
- Unidirectional: Server-to-client only
- HTTP-based: Uses standard HTTP, works through proxies and firewalls
- Automatic reconnection: Browser handles reconnection automatically
- Text-only: Can only send text data (though JSON can be encoded)
- Simple: Easier to implement than WebSockets
Client-Side SSE Implementation
// Create EventSource connectionconst eventSource = new EventSource("/api/events");
// Listen for messageseventSource.onmessage = (event) => { const data = JSON.parse(event.data); console.log("Received:", data);
// Update UI based on event type if (data.type === "notification") { showNotification(data.message); } else if (data.type === "update") { updateDashboard(data.metrics); }};
// Listen for specific event typeseventSource.addEventListener("userJoined", (event) => { const user = JSON.parse(event.data); addUserToList(user);});
eventSource.addEventListener("systemAlert", (event) => { const alert = JSON.parse(event.data); showAlert(alert.message, alert.severity);});
// Handle connection openedeventSource.onopen = () => { console.log("SSE connection opened"); updateConnectionStatus("connected");};
// Handle errorseventSource.onerror = (error) => { console.error("SSE error:", error); updateConnectionStatus("error"); // EventSource automatically attempts to reconnect};
// Close connection when donefunction closeConnection() { eventSource.close();}Server-Side SSE Implementation
// Node.js Express exampleconst express = require("express");const app = express();
// Store active SSE connectionsconst clients = new Set();
// SSE endpointapp.get("/api/events", (req, res) => { // Set headers for SSE res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); res.setHeader("X-Accel-Buffering", "no"); // Disable buffering in nginx
// Add client to set clients.add(res);
// Send initial connection message res.write( `data: ${JSON.stringify({ type: "connected", timestamp: Date.now(), })}\n\n`, );
// Handle client disconnect req.on("close", () => { clients.delete(res); console.log("Client disconnected"); });});
// Function to broadcast to all connected clientsfunction broadcast(data) { const message = `data: ${JSON.stringify(data)}\n\n`; clients.forEach((client) => { try { client.write(message); } catch (error) { // Remove dead connections clients.delete(client); } });}
// Example: Send notificationfunction sendNotification(message) { broadcast({ type: "notification", message: message, timestamp: Date.now(), });}
// Example: Send custom event typefunction sendCustomEvent(eventType, data) { const message = `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`; clients.forEach((client) => { try { client.write(message); } catch (error) { clients.delete(client); } });}
app.listen(3000, () => { console.log("Server running on port 3000");});SSE Message Format
SSE messages follow a specific format:
// Simple messageres.write("data: Hello World\n\n");
// Message with ID (for reconnection)res.write('id: 12345\ndata: {"type": "update"}\n\n');
// Custom event typeres.write('event: userJoined\ndata: {"userId": "123"}\n\n');
// Multiple data lines (joined with \n)res.write("data: Line 1\ndata: Line 2\ndata: Line 3\n\n");
// Comments (ignored by client)res.write(": This is a comment\n");SSE Advantages ✅
- Simple implementation: Easier than WebSockets
- Automatic reconnection: Browser handles reconnection automatically
- HTTP-based: Works through proxies and firewalls
- Built-in event types: Support for custom event types
- Lower server overhead: Simpler than WebSocket connections
SSE Disadvantages ❌
- Unidirectional: Cannot send data from client to server (must use separate HTTP requests)
- Text-only: Limited to text data (though JSON works)
- Connection limits: Browsers limit concurrent connections per domain
- No binary support: Cannot send binary data efficiently
HTTP/2 Server Push: Resource Preloading
HTTP/2 Server Push allows servers to proactively send resources to clients before they’re requested. However, it’s important to note that HTTP/2 Server Push has been deprecated in HTTP/3 and is being replaced by alternative approaches.
How HTTP/2 Server Push Works
When a server receives a request for an HTML page, it can push additional resources (CSS, JavaScript, images) that it knows the client will need. This reduces round trips and improves page load performance.
Key Characteristics:
- Resource preloading: Pushes resources before client requests them
- HTTP/2 feature: Requires HTTP/2 connection
- One-time push: Resources are pushed once per connection
- Cache-aware: Respects browser cache
- Deprecated: Removed in HTTP/3 in favor of alternative approaches
HTTP/2 Server Push Implementation
// Node.js with http2const http2 = require("http2");const fs = require("fs");
const server = http2.createSecureServer({ key: fs.readFileSync("server.key"), cert: fs.readFileSync("server.cert"),});
server.on("stream", (stream, headers) => { const path = headers[":path"];
if (path === "/index.html") { // Push CSS file stream.pushStream({ ":path": "/styles.css" }, (err, pushStream) => { if (!err) { pushStream.respond({ ":status": 200, "content-type": "text/css" }); pushStream.end(fs.readFileSync("styles.css")); } });
// Push JavaScript file stream.pushStream({ ":path": "/app.js" }, (err, pushStream) => { if (!err) { pushStream.respond({ ":status": 200, "content-type": "application/javascript", }); pushStream.end(fs.readFileSync("app.js")); } });
// Send the HTML stream.respond({ ":status": 200, "content-type": "text/html" }); stream.end(fs.readFileSync("index.html")); } else { // Handle other requests normally stream.respond({ ":status": 404 }); stream.end(); }});
server.listen(8443);HTTP/2 Server Push Considerations ⚠️
Important Notes:
- Deprecated in HTTP/3: HTTP/2 Server Push is not available in HTTP/3
- Cache complexity: Can push resources that are already cached
- Bandwidth waste: May push resources the client doesn’t need
- Limited use cases: Primarily useful for initial page loads
- Alternative approaches: Consider
<link rel="preload">or Early Hints instead
Modern Alternatives to Server Push
<!-- Preload critical resources --><link rel="preload" href="/critical.css" as="style" /><link rel="preload" href="/app.js" as="script" />
<!-- Preconnect to external domains --><link rel="preconnect" href="https://api.example.com" />
<!-- DNS prefetch --><link rel="dns-prefetch" href="https://cdn.example.com" />Comparing the Technologies
Understanding the differences between these technologies helps you choose the right one for your use case.
Feature Comparison Table
| Feature | WebSockets | Server-Sent Events | HTTP/2 Push |
|---|---|---|---|
| Direction | Bidirectional | Unidirectional (server→client) | Unidirectional (server→client) |
| Protocol | ws:// or wss:// | HTTP/HTTPS | HTTP/2 |
| Connection | Persistent TCP | Persistent HTTP | Per-request |
| Reconnection | Manual | Automatic | N/A |
| Data Format | Text & Binary | Text only | Any resource |
| Overhead | Low (after handshake) | Medium (HTTP headers) | Medium (HTTP/2 frames) |
| Browser Support | Excellent | Excellent | HTTP/2 required |
| Proxy/Firewall | May be blocked | Usually works | Usually works |
| Use Case | Real-time apps, games | Notifications, live feeds | Resource preloading |
| Complexity | High | Low | Medium |
Communication Patterns
WebSockets - Full-duplex communication:
Client ←→ Server ↑ ↓ └────────┘Server-Sent Events - One-way server push:
Client ← Server ↑ ↓ └───────┘(Client uses separate HTTP requests to send data)HTTP/2 Push - Resource preloading:
Client → Server (request HTML)Server → Client (push CSS, JS, images)Performance Characteristics
Latency:
- WebSockets: Lowest latency after connection established (~1-2ms per message)
- SSE: Low latency (~10-50ms depending on network)
- HTTP/2 Push: Reduces initial page load time, not for ongoing communication
Bandwidth Efficiency:
- WebSockets: Most efficient for frequent small messages
- SSE: Efficient for one-way server updates
- HTTP/2 Push: Can waste bandwidth if resources are already cached
Server Resources:
- WebSockets: Higher memory per connection (stateful)
- SSE: Lower memory per connection (stateless HTTP)
- HTTP/2 Push: Minimal overhead (per-request)
Performance Considerations
Understanding performance implications helps you optimize your real-time applications.
Connection Overhead
WebSocket Handshake:
// Initial handshake includes HTTP upgrade request// Overhead: ~200-300 bytes// After handshake: 2-14 bytes per frame
// Example: Sending 1000 messages// WebSocket: ~200 bytes (handshake) + 2000-14000 bytes (messages)// Total: ~2200-14200 bytesSSE Connection:
// Each SSE connection maintains HTTP headers// Overhead: ~400-600 bytes per connection// Per message: ~50-100 bytes (HTTP framing)
// Example: Sending 1000 messages// SSE: ~600 bytes (connection) + 50000-100000 bytes (messages)// Total: ~50600-100600 bytesScalability Patterns
WebSocket Scaling:
// Horizontal scaling requires sticky sessions or message brokerconst Redis = require("ioredis");const redis = new Redis();
// Publish message to all serversfunction broadcastToAllServers(message) { redis.publish("messages", JSON.stringify(message));}
// Each server subscribes and broadcasts to its clientsredis.subscribe("messages");redis.on("message", (channel, message) => { const data = JSON.parse(message); broadcastToClients(data);});SSE Scaling:
// SSE can scale more easily with load balancers// Stateless HTTP connections work well with round-robin
// Use Redis pub/sub for cross-server communicationconst redis = require("ioredis");const subscriber = new Redis();
subscriber.subscribe("events");subscriber.on("message", (channel, message) => { const event = JSON.parse(message); broadcastToSSEClients(event);});Memory and CPU Usage
WebSocket Memory:
- Each connection: ~2-10 KB (depending on buffers)
- 10,000 connections: ~20-100 MB
- CPU: Higher due to connection management
SSE Memory:
- Each connection: ~1-5 KB
- 10,000 connections: ~10-50 MB
- CPU: Lower, simpler HTTP handling
Use Cases and When to Choose
Choosing the right technology depends on your specific requirements.
When to Use WebSockets ✅
Ideal for:
- Chat applications: Bidirectional messaging required
- Collaborative editing: Real-time synchronization needed
- Online games: Low latency, bidirectional communication
- Trading platforms: High-frequency updates, bidirectional
- Video/voice calling: WebRTC signaling, bidirectional control
Example Use Case - Chat Application:
// WebSocket is perfect for chatconst ws = new WebSocket("wss://chat.example.com");
// User sends messagefunction sendMessage(text) { ws.send( JSON.stringify({ type: "message", content: text, room: currentRoom, }), );}
// Receive messages in real-timews.onmessage = (event) => { const message = JSON.parse(event.data); displayMessage(message);};When to Use Server-Sent Events ✅
Ideal for:
- Live notifications: One-way server updates
- Dashboard updates: Real-time metrics and charts
- Live feeds: News, social media feeds, stock prices
- Progress updates: Long-running task progress
- Event streams: Log streaming, audit trails
Example Use Case - Live Dashboard:
// SSE is perfect for one-way updatesconst eventSource = new EventSource("/api/metrics");
eventSource.onmessage = (event) => { const metrics = JSON.parse(event.data); updateChart(metrics.cpu); updateChart(metrics.memory); updateChart(metrics.network);};
// User actions use regular HTTP requestsasync function refreshData() { const response = await fetch("/api/refresh", { method: "POST" }); // SSE will push the updated metrics}When to Use HTTP/2 Push ✅
Ideal for:
- Initial page loads: Preloading critical CSS/JS
- Resource optimization: Reducing round trips
- CDN integration: Pushing assets from edge servers
Note: ⚠️ HTTP/2 Push is deprecated in HTTP/3. Consider alternatives like <link rel="preload"> or Early Hints instead.
Decision Matrix
| Requirement | WebSockets | SSE | HTTP/2 Push |
|---|---|---|---|
| Bidirectional communication | ✅ Yes | ❌ No | ❌ No |
| Server-to-client only | ⚠️ Overkill | ✅ Perfect | ❌ Wrong use case |
| Low latency | ✅ Best | ✅ Good | ❌ N/A |
| Simple implementation | ❌ Complex | ✅ Simple | ⚠️ Medium |
| Works through proxies | ⚠️ May fail | ✅ Yes | ✅ Yes |
| Automatic reconnection | ❌ Manual | ✅ Automatic | ❌ N/A |
| Resource preloading | ❌ No | ❌ No | ✅ Yes |
Implementation Examples
Let’s explore complete implementation examples for common scenarios.
Example 1: Real-time Chat with WebSockets
Client Implementation:
class ChatClient { constructor(url) { this.ws = null; this.url = url; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; }
connect() { this.ws = new WebSocket(this.url);
this.ws.onopen = () => { console.log("Connected to chat"); this.reconnectAttempts = 0; this.onConnected(); };
this.ws.onmessage = (event) => { const message = JSON.parse(event.data); this.handleMessage(message); };
this.ws.onerror = (error) => { console.error("WebSocket error:", error); this.onError(error); };
this.ws.onclose = () => { console.log("Connection closed"); this.onDisconnected(); this.attemptReconnect(); }; }
sendMessage(text, room) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send( JSON.stringify({ type: "message", content: text, room: room, timestamp: Date.now(), }), ); } }
attemptReconnect() { if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000); setTimeout(() => this.connect(), delay); } }
handleMessage(message) { switch (message.type) { case "message": this.displayMessage(message); break; case "userJoined": this.addUser(message.user); break; case "userLeft": this.removeUser(message.userId); break; case "error": this.showError(message.message); break; } }
onConnected() { /* Override in subclass */ } onDisconnected() { /* Override in subclass */ } onError(error) { /* Override in subclass */ } displayMessage(message) { /* Override in subclass */ } addUser(user) { /* Override in subclass */ } removeUser(userId) { /* Override in subclass */ } showError(message) { /* Override in subclass */ }}Server Implementation:
const WebSocket = require("ws");const wss = new WebSocket.Server({ port: 8080 });
const rooms = new Map(); // roomId -> Set of clients
wss.on("connection", (ws, req) => { let currentRoom = null; let userId = null;
ws.on("message", (message) => { try { const data = JSON.parse(message);
switch (data.type) { case "join": userId = data.userId; currentRoom = data.room;
if (!rooms.has(currentRoom)) { rooms.set(currentRoom, new Set()); } rooms.get(currentRoom).add(ws);
// Notify others broadcastToRoom( currentRoom, { type: "userJoined", userId: userId, }, ws, );
ws.send( JSON.stringify({ type: "joined", room: currentRoom, }), ); break;
case "message": if (currentRoom) { broadcastToRoom( currentRoom, { type: "message", userId: userId, content: data.content, timestamp: Date.now(), }, ws, ); } break;
case "leave": if (currentRoom && rooms.has(currentRoom)) { rooms.get(currentRoom).delete(ws); broadcastToRoom( currentRoom, { type: "userLeft", userId: userId, }, ws, ); currentRoom = null; } break; } } catch (error) { ws.send( JSON.stringify({ type: "error", message: "Invalid message format", }), ); } });
ws.on("close", () => { if (currentRoom && rooms.has(currentRoom)) { rooms.get(currentRoom).delete(ws); broadcastToRoom( currentRoom, { type: "userLeft", userId: userId, }, ws, ); } });});
function broadcastToRoom(roomId, message, excludeWs = null) { if (!rooms.has(roomId)) return;
const messageStr = JSON.stringify(message); rooms.get(roomId).forEach((client) => { if (client !== excludeWs && client.readyState === WebSocket.OPEN) { client.send(messageStr); } });}Example 2: Live Notifications with SSE
Client Implementation:
class NotificationService { constructor() { this.eventSource = null; this.listeners = new Map(); }
connect() { this.eventSource = new EventSource("/api/notifications");
this.eventSource.onopen = () => { console.log("Notification service connected"); };
this.eventSource.onmessage = (event) => { const notification = JSON.parse(event.data); this.handleNotification(notification); };
// Listen for specific event types this.eventSource.addEventListener("alert", (event) => { const alert = JSON.parse(event.data); this.showAlert(alert); });
this.eventSource.addEventListener("update", (event) => { const update = JSON.parse(event.data); this.handleUpdate(update); });
this.eventSource.onerror = (error) => { console.error("SSE error:", error); // EventSource automatically reconnects }; }
handleNotification(notification) { // Show notification UI this.showNotification(notification);
// Trigger registered listeners const listeners = this.listeners.get(notification.type) || []; listeners.forEach((listener) => listener(notification)); }
on(type, callback) { if (!this.listeners.has(type)) { this.listeners.set(type, []); } this.listeners.get(type).push(callback); }
disconnect() { if (this.eventSource) { this.eventSource.close(); this.eventSource = null; } }
showNotification(notification) { // Implementation depends on your UI framework console.log("Notification:", notification); }
showAlert(alert) { // Show alert UI console.log("Alert:", alert); }
handleUpdate(update) { // Handle update console.log("Update:", update); }}
// Usageconst notifications = new NotificationService();notifications.connect();
notifications.on("message", (notification) => { updateMessageBadge(notification.count);});Server Implementation:
const express = require("express");const app = express();
const notificationClients = new Set();
app.get("/api/notifications", (req, res) => { res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); res.setHeader("X-Accel-Buffering", "no");
notificationClients.add(res);
// Send initial connection confirmation res.write( `data: ${JSON.stringify({ type: "connected", timestamp: Date.now(), })}\n\n`, );
req.on("close", () => { notificationClients.delete(res); });});
// Function to send notification to all clientsfunction sendNotification(type, data) { const message = `data: ${JSON.stringify({ type: type, ...data, timestamp: Date.now(), })}\n\n`;
notificationClients.forEach((client) => { try { client.write(message); } catch (error) { notificationClients.delete(client); } });}
// Function to send custom event typefunction sendCustomEvent(eventType, data) { const message = `event: ${eventType}\ndata: ${JSON.stringify({ ...data, timestamp: Date.now(), })}\n\n`;
notificationClients.forEach((client) => { try { client.write(message); } catch (error) { notificationClients.delete(client); } });}
// Example: Send notification when user receives a messageapp.post("/api/send-notification", (req, res) => { const { userId, message } = req.body;
sendNotification("message", { userId: userId, message: message, });
res.json({ success: true });});
// Example: Send alertfunction sendAlert(severity, message) { sendCustomEvent("alert", { severity: severity, message: message, });}
app.listen(3000);Example 3: Hybrid Approach
Sometimes you need both WebSockets and SSE:
// Use WebSockets for bidirectional chatconst chatWs = new WebSocket("wss://api.example.com/chat");
// Use SSE for one-way notificationsconst notifications = new EventSource("https://api.example.com/notifications");
// Chat messages (bidirectional)chatWs.onmessage = (event) => { const message = JSON.parse(event.data); displayChatMessage(message);};
function sendChatMessage(text) { chatWs.send(JSON.stringify({ content: text }));}
// Notifications (one-way)notifications.onmessage = (event) => { const notification = JSON.parse(event.data); showNotification(notification);};Best Practices and Common Pitfalls
Following best practices helps you build reliable, scalable real-time applications.
WebSocket Best Practices ✅
1. Implement Proper Reconnection Logic:
class RobustWebSocket { constructor(url) { this.url = url; this.ws = null; this.reconnectTimeout = null; this.reconnectDelay = 1000; this.maxReconnectDelay = 30000; this.shouldReconnect = true; }
connect() { this.ws = new WebSocket(this.url);
this.ws.onopen = () => { this.reconnectDelay = 1000; // Reset delay on successful connection this.onOpen(); };
this.ws.onclose = () => { this.onClose(); if (this.shouldReconnect) { this.scheduleReconnect(); } };
this.ws.onerror = (error) => { this.onError(error); }; }
scheduleReconnect() { if (this.reconnectTimeout) return;
this.reconnectTimeout = setTimeout(() => { this.reconnectTimeout = null; console.log(`Reconnecting in ${this.reconnectDelay}ms...`); this.connect(); this.reconnectDelay = Math.min( this.reconnectDelay * 2, this.maxReconnectDelay, ); }, this.reconnectDelay); }
disconnect() { this.shouldReconnect = false; if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); } if (this.ws) { this.ws.close(); } }
onOpen() { /* Override */ } onClose() { /* Override */ } onError(error) { /* Override */ }}2. Handle Message Queuing:
class WebSocketWithQueue { constructor(url) { this.ws = null; this.messageQueue = []; this.url = url; }
connect() { this.ws = new WebSocket(this.url); this.ws.onopen = () => { // Send queued messages while (this.messageQueue.length > 0) { const message = this.messageQueue.shift(); this.ws.send(message); } }; }
send(data) { const message = JSON.stringify(data);
if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(message); } else { // Queue message if not connected this.messageQueue.push(message); } }}3. Implement Heartbeat/Ping-Pong:
// Server-side heartbeatsetInterval(() => { clients.forEach((client) => { if (client.isAlive === false) { return client.terminate(); } client.isAlive = false; client.ping(); });}, 30000);
ws.on("pong", () => { ws.isAlive = true;});SSE Best Practices ✅
1. Handle Connection Limits:
// Browsers limit concurrent SSE connections per domain// Solution: Use a single SSE connection with multiple event types
const eventSource = new EventSource("/api/events");
// Instead of multiple connections, use event typeseventSource.addEventListener("notifications", handleNotifications);eventSource.addEventListener("updates", handleUpdates);eventSource.addEventListener("alerts", handleAlerts);2. Implement Proper Error Handling:
const eventSource = new EventSource("/api/events");
eventSource.onerror = (error) => { // EventSource automatically reconnects // But you should handle errors gracefully if (eventSource.readyState === EventSource.CLOSED) { console.error("SSE connection closed"); // Implement fallback or user notification }};3. Use Event IDs for Reconnection:
// Server-side: Include event IDsres.write(`id: ${eventId}\ndata: ${JSON.stringify(data)}\n\n`);
// Client-side: Track last received IDlet lastEventId = null;
eventSource.onmessage = (event) => { lastEventId = event.lastEventId; // Process message};
// On reconnection, EventSource automatically sends Last-Event-ID headerCommon Pitfalls ❌
1. Not Handling Disconnections:
// ❌ Bad: No reconnection logicconst ws = new WebSocket("wss://api.example.com");ws.onclose = () => { // Connection lost forever};
// ✅ Good: Implement reconnectionconst ws = new WebSocket("wss://api.example.com");ws.onclose = () => { setTimeout(() => reconnect(), 1000);};2. Sending Messages Before Connection Ready:
// ❌ Bad: May fail if connection not readyconst ws = new WebSocket("wss://api.example.com");ws.send("Hello"); // May fail
// ✅ Good: Check ready stateif (ws.readyState === WebSocket.OPEN) { ws.send("Hello");}3. Not Cleaning Up Connections:
// ❌ Bad: Memory leaksfunction createConnection() { const ws = new WebSocket("wss://api.example.com"); // Never closed}
// ✅ Good: Clean up on component unmountfunction createConnection() { const ws = new WebSocket("wss://api.example.com");
return () => { ws.close(); };}4. Ignoring Browser Connection Limits:
// ❌ Bad: Multiple SSE connectionsconst notifications = new EventSource("/api/notifications");const updates = new EventSource("/api/updates");const alerts = new EventSource("/api/alerts");// May hit browser connection limit (usually 6 per domain)
// ✅ Good: Single connection with event typesconst eventSource = new EventSource("/api/events");eventSource.addEventListener("notifications", handleNotifications);eventSource.addEventListener("updates", handleUpdates);eventSource.addEventListener("alerts", handleAlerts);Security Considerations 🔒
1. Authentication:
// WebSocket authenticationconst ws = new WebSocket("wss://api.example.com", { headers: { Authorization: `Bearer ${token}`, },});
// SSE authentication (via query parameter or cookie)const eventSource = new EventSource("/api/events?token=" + token);2. Rate Limiting:
// Server-side rate limitingconst rateLimiter = new Map();
function checkRateLimit(clientId) { const now = Date.now(); const windowStart = now - 60000; // 1 minute window
if (!rateLimiter.has(clientId)) { rateLimiter.set(clientId, []); }
const requests = rateLimiter .get(clientId) .filter((time) => time > windowStart);
if (requests.length >= 100) { return false; // Rate limit exceeded }
requests.push(now); rateLimiter.set(clientId, requests); return true;}3. Input Validation:
// Always validate and sanitize inputws.on("message", (message) => { try { const data = JSON.parse(message);
// Validate data structure if (!data.type || !data.content) { throw new Error("Invalid message format"); }
// Sanitize content const sanitized = sanitizeInput(data.content);
// Process message handleMessage({ ...data, content: sanitized }); } catch (error) { ws.send(JSON.stringify({ type: "error", message: error.message })); }});Conclusion
Choosing the right real-time communication technology depends on your specific requirements. WebSockets excel at bidirectional communication for applications like chat, gaming, and collaborative tools. Server-Sent Events are perfect for one-way server-to-client updates like notifications, live feeds, and dashboard updates. HTTP/2 Server Push (though deprecated in HTTP/3) was designed for resource preloading, but modern alternatives like <link rel="preload"> are preferred.
Key Takeaways:
- WebSockets: Use for bidirectional, low-latency communication requiring full-duplex channels
- Server-Sent Events: Use for simple, one-way server-to-client updates with automatic reconnection
- HTTP/2 Push: Deprecated in HTTP/3; use modern alternatives like preload hints instead
- Hybrid approaches: Don’t hesitate to combine technologies when appropriate
- Always implement: Proper error handling, reconnection logic, and security measures
Next Steps:
- Explore WebSocket libraries like Socket.io for additional features (rooms, namespaces, fallbacks)
- Learn about GraphQL Subscriptions for real-time data in GraphQL APIs
- Consider message brokers like Redis Pub/Sub for scaling real-time applications across servers
- Review error handling strategies for robust real-time applications
- Study web performance optimization techniques to minimize latency
Remember, the best technology is the one that fits your use case, team expertise, and scalability requirements. Start simple with SSE if you only need one-way communication, and upgrade to WebSockets when you need bidirectional capabilities. Always test your implementation under realistic load conditions and monitor performance metrics to ensure your real-time communication solution meets your application’s needs.