Back to Blog
WebSocketsSSEReal-timeWeb Development

WebSockets vs Server-Sent Events: When to Use Each

Choose the right real-time technology. Compare WebSockets and SSE for bidirectional vs unidirectional communication.

B
Bootspring Team
Engineering
March 12, 2024
5 min read

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.

Share this article

Help spread the word about Bootspring