Real-time features need the right technology. WebSockets provide bidirectional communication, while Server-Sent Events (SSE) excel at server-to-client streaming.
Quick Comparison#
WebSockets:
- Bidirectional (client ↔ server)
- Binary and text data
- Custom protocol
- More complex setup
- Use for: Chat, gaming, collaboration
SSE (Server-Sent Events):
- Unidirectional (server → client)
- Text data only
- Built on HTTP
- Simpler implementation
- Use for: Notifications, feeds, dashboards
Server-Sent Events (SSE)#
Server Implementation#
1// Express SSE endpoint
2app.get('/events', (req, res) => {
3 // Set SSE headers
4 res.setHeader('Content-Type', 'text/event-stream');
5 res.setHeader('Cache-Control', 'no-cache');
6 res.setHeader('Connection', 'keep-alive');
7
8 // Send initial connection event
9 res.write('event: connected\n');
10 res.write('data: {"status": "connected"}\n\n');
11
12 // Send periodic updates
13 const interval = setInterval(() => {
14 const data = JSON.stringify({
15 time: new Date().toISOString(),
16 value: Math.random(),
17 });
18 res.write(`data: ${data}\n\n`);
19 }, 1000);
20
21 // Clean up on client disconnect
22 req.on('close', () => {
23 clearInterval(interval);
24 res.end();
25 });
26});Client Implementation#
1// Browser EventSource API
2const eventSource = new EventSource('/events');
3
4eventSource.onopen = () => {
5 console.log('Connected to SSE');
6};
7
8eventSource.onmessage = (event) => {
9 const data = JSON.parse(event.data);
10 console.log('Received:', data);
11};
12
13eventSource.addEventListener('connected', (event) => {
14 console.log('Custom event:', event.data);
15});
16
17eventSource.onerror = (error) => {
18 console.error('SSE error:', error);
19 // Auto-reconnects by default
20};
21
22// Close connection
23eventSource.close();SSE with Reconnection#
1// Custom SSE client with retry logic
2class SSEClient {
3 private eventSource: EventSource | null = null;
4 private retryCount = 0;
5 private maxRetries = 5;
6
7 constructor(private url: string, private handlers: EventHandlers) {}
8
9 connect() {
10 this.eventSource = new EventSource(this.url);
11
12 this.eventSource.onopen = () => {
13 this.retryCount = 0;
14 this.handlers.onOpen?.();
15 };
16
17 this.eventSource.onmessage = (event) => {
18 this.handlers.onMessage?.(JSON.parse(event.data));
19 };
20
21 this.eventSource.onerror = () => {
22 this.eventSource?.close();
23
24 if (this.retryCount < this.maxRetries) {
25 const delay = Math.pow(2, this.retryCount) * 1000;
26 this.retryCount++;
27 setTimeout(() => this.connect(), delay);
28 } else {
29 this.handlers.onError?.(new Error('Max retries exceeded'));
30 }
31 };
32 }
33
34 close() {
35 this.eventSource?.close();
36 this.eventSource = null;
37 }
38}WebSockets#
Server Implementation#
1// Using ws library
2import { WebSocketServer, WebSocket } from 'ws';
3
4const wss = new WebSocketServer({ port: 8080 });
5
6const clients = new Set<WebSocket>();
7
8wss.on('connection', (ws) => {
9 clients.add(ws);
10 console.log('Client connected');
11
12 // Send welcome message
13 ws.send(JSON.stringify({ type: 'welcome', message: 'Connected!' }));
14
15 // Handle incoming messages
16 ws.on('message', (data) => {
17 const message = JSON.parse(data.toString());
18 console.log('Received:', message);
19
20 // Broadcast to all clients
21 clients.forEach((client) => {
22 if (client.readyState === WebSocket.OPEN) {
23 client.send(JSON.stringify({
24 type: 'broadcast',
25 from: 'server',
26 data: message,
27 }));
28 }
29 });
30 });
31
32 ws.on('close', () => {
33 clients.delete(ws);
34 console.log('Client disconnected');
35 });
36
37 ws.on('error', (error) => {
38 console.error('WebSocket error:', error);
39 });
40});Client Implementation#
1// Browser WebSocket API
2class WebSocketClient {
3 private ws: WebSocket | null = null;
4 private reconnectAttempts = 0;
5 private maxReconnectAttempts = 5;
6 private messageQueue: string[] = [];
7
8 constructor(
9 private url: string,
10 private handlers: {
11 onMessage: (data: any) => void;
12 onOpen?: () => void;
13 onClose?: () => void;
14 onError?: (error: Event) => void;
15 }
16 ) {}
17
18 connect() {
19 this.ws = new WebSocket(this.url);
20
21 this.ws.onopen = () => {
22 this.reconnectAttempts = 0;
23 this.handlers.onOpen?.();
24
25 // Send queued messages
26 while (this.messageQueue.length > 0) {
27 const message = this.messageQueue.shift();
28 if (message) this.ws?.send(message);
29 }
30 };
31
32 this.ws.onmessage = (event) => {
33 const data = JSON.parse(event.data);
34 this.handlers.onMessage(data);
35 };
36
37 this.ws.onclose = () => {
38 this.handlers.onClose?.();
39 this.reconnect();
40 };
41
42 this.ws.onerror = (error) => {
43 this.handlers.onError?.(error);
44 };
45 }
46
47 send(data: object) {
48 const message = JSON.stringify(data);
49
50 if (this.ws?.readyState === WebSocket.OPEN) {
51 this.ws.send(message);
52 } else {
53 this.messageQueue.push(message);
54 }
55 }
56
57 private reconnect() {
58 if (this.reconnectAttempts < this.maxReconnectAttempts) {
59 const delay = Math.pow(2, this.reconnectAttempts) * 1000;
60 this.reconnectAttempts++;
61 setTimeout(() => this.connect(), delay);
62 }
63 }
64
65 close() {
66 this.ws?.close();
67 }
68}
69
70// Usage
71const client = new WebSocketClient('wss://api.example.com/ws', {
72 onMessage: (data) => console.log('Message:', data),
73 onOpen: () => console.log('Connected'),
74 onClose: () => console.log('Disconnected'),
75});
76
77client.connect();
78client.send({ type: 'subscribe', channel: 'updates' });Use Case Comparison#
Use SSE for:
✓ Live dashboards
✓ Stock tickers
✓ News feeds
✓ Notification streams
✓ Progress updates
✓ Log streaming
Use WebSockets for:
✓ Chat applications
✓ Multiplayer games
✓ Collaborative editing
✓ Live auctions
✓ Trading platforms
✓ IoT device control
Performance Comparison#
SSE:
- Lower overhead (HTTP)
- Auto-reconnection built-in
- Works through proxies/firewalls
- Simpler to scale (stateless)
WebSockets:
- Lower latency (persistent connection)
- Bidirectional communication
- Binary data support
- More efficient for high-frequency updates
Scaling Considerations#
1// SSE scaling with Redis pub/sub
2import Redis from 'ioredis';
3
4const redis = new Redis();
5const subscriber = new Redis();
6
7app.get('/events', async (req, res) => {
8 res.setHeader('Content-Type', 'text/event-stream');
9
10 const channel = 'updates';
11 await subscriber.subscribe(channel);
12
13 subscriber.on('message', (ch, message) => {
14 if (ch === channel) {
15 res.write(`data: ${message}\n\n`);
16 }
17 });
18
19 req.on('close', () => {
20 subscriber.unsubscribe(channel);
21 res.end();
22 });
23});
24
25// Publish from anywhere
26await redis.publish('updates', JSON.stringify({ type: 'notification', data: '...' }));Decision Guide#
Questions to ask:
1. Does the client need to send data frequently?
Yes → WebSockets
No → SSE
2. Do you need binary data?
Yes → WebSockets
No → Either works
3. Is simplicity important?
Yes → SSE
No → Either works
4. Are you behind restrictive firewalls?
Yes → SSE (uses standard HTTP)
No → Either works
5. Do you need sub-100ms latency?
Yes → WebSockets
No → Either works
Conclusion#
SSE is simpler and sufficient for server-to-client streaming. WebSockets are necessary when you need bidirectional communication or binary data.
Start with SSE if it meets your needs—upgrade to WebSockets when bidirectional communication becomes essential.