Back to Blog
SSEReal-timeStreamingNode.js

Server-Sent Events for Real-time Updates

Implement SSE for server-to-client streaming. From basic setup to reconnection handling to scaling strategies.

B
Bootspring Team
Engineering
January 11, 2022
6 min read

Server-Sent Events (SSE) provide a simple way to stream data from server to client. Here's how to implement them effectively.

Basic SSE Server#

1import express from 'express'; 2 3const app = express(); 4 5// SSE endpoint 6app.get('/events', (req, res) => { 7 // Set SSE headers 8 res.setHeader('Content-Type', 'text/event-stream'); 9 res.setHeader('Cache-Control', 'no-cache'); 10 res.setHeader('Connection', 'keep-alive'); 11 12 // Disable response buffering 13 res.flushHeaders(); 14 15 // Send initial connection message 16 res.write('event: connected\n'); 17 res.write('data: {"status": "connected"}\n\n'); 18 19 // Send periodic updates 20 const intervalId = setInterval(() => { 21 const data = { 22 timestamp: new Date().toISOString(), 23 value: Math.random(), 24 }; 25 26 res.write(`data: ${JSON.stringify(data)}\n\n`); 27 }, 1000); 28 29 // Clean up on client disconnect 30 req.on('close', () => { 31 clearInterval(intervalId); 32 res.end(); 33 }); 34}); 35 36app.listen(3000);

SSE Message Format#

1// SSE message format 2function sendEvent( 3 res: express.Response, 4 event: string, 5 data: any, 6 id?: string 7) { 8 if (id) { 9 res.write(`id: ${id}\n`); 10 } 11 res.write(`event: ${event}\n`); 12 res.write(`data: ${JSON.stringify(data)}\n\n`); 13} 14 15// Examples 16sendEvent(res, 'message', { text: 'Hello' }, '1'); 17sendEvent(res, 'notification', { title: 'New alert' }, '2'); 18 19// Multi-line data 20res.write(`data: line 1\n`); 21res.write(`data: line 2\n`); 22res.write(`data: line 3\n\n`); 23 24// Retry interval (milliseconds) 25res.write(`retry: 5000\n\n`);

Client Implementation#

1// Basic EventSource 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('Message:', data); 11}; 12 13eventSource.onerror = (error) => { 14 console.error('SSE error:', error); 15 if (eventSource.readyState === EventSource.CLOSED) { 16 console.log('Connection closed'); 17 } 18}; 19 20// Named events 21eventSource.addEventListener('notification', (event) => { 22 const data = JSON.parse(event.data); 23 showNotification(data); 24}); 25 26eventSource.addEventListener('update', (event) => { 27 const data = JSON.parse(event.data); 28 updateUI(data); 29}); 30 31// Close connection 32eventSource.close();

React Hook#

1import { useEffect, useState, useCallback } from 'react'; 2 3interface SSEOptions { 4 onMessage?: (data: any) => void; 5 onError?: (error: Event) => void; 6 events?: Record<string, (data: any) => void>; 7} 8 9function useSSE(url: string, options: SSEOptions = {}) { 10 const [isConnected, setIsConnected] = useState(false); 11 const [lastEvent, setLastEvent] = useState<any>(null); 12 13 useEffect(() => { 14 const eventSource = new EventSource(url); 15 16 eventSource.onopen = () => { 17 setIsConnected(true); 18 }; 19 20 eventSource.onmessage = (event) => { 21 const data = JSON.parse(event.data); 22 setLastEvent(data); 23 options.onMessage?.(data); 24 }; 25 26 eventSource.onerror = (error) => { 27 setIsConnected(false); 28 options.onError?.(error); 29 }; 30 31 // Named event listeners 32 if (options.events) { 33 Object.entries(options.events).forEach(([eventName, handler]) => { 34 eventSource.addEventListener(eventName, (event) => { 35 const data = JSON.parse((event as MessageEvent).data); 36 handler(data); 37 }); 38 }); 39 } 40 41 return () => { 42 eventSource.close(); 43 }; 44 }, [url]); 45 46 return { isConnected, lastEvent }; 47} 48 49// Usage 50function Dashboard() { 51 const [notifications, setNotifications] = useState<Notification[]>([]); 52 53 const { isConnected } = useSSE('/api/events', { 54 events: { 55 notification: (data) => { 56 setNotifications((prev) => [...prev, data]); 57 }, 58 update: (data) => { 59 // Handle updates 60 }, 61 }, 62 }); 63 64 return ( 65 <div> 66 <span>{isConnected ? '🟢' : '🔴'}</span> 67 <NotificationList notifications={notifications} /> 68 </div> 69 ); 70}

Authentication and Headers#

1// SSE doesn't support custom headers in EventSource 2// Solution 1: Query parameters 3const eventSource = new EventSource(`/events?token=${token}`); 4 5// Solution 2: Cookies (if same-origin) 6// Token stored in httpOnly cookie 7 8// Solution 3: Use fetch with ReadableStream 9async function connectSSE(url: string, token: string) { 10 const response = await fetch(url, { 11 headers: { 12 'Authorization': `Bearer ${token}`, 13 'Accept': 'text/event-stream', 14 }, 15 }); 16 17 const reader = response.body?.getReader(); 18 const decoder = new TextDecoder(); 19 20 while (true) { 21 const { done, value } = await reader!.read(); 22 if (done) break; 23 24 const text = decoder.decode(value); 25 const lines = text.split('\n'); 26 27 for (const line of lines) { 28 if (line.startsWith('data: ')) { 29 const data = JSON.parse(line.slice(6)); 30 handleMessage(data); 31 } 32 } 33 } 34} 35 36// Server-side with auth 37app.get('/events', authenticateToken, (req, res) => { 38 const userId = req.user.id; 39 40 res.setHeader('Content-Type', 'text/event-stream'); 41 res.flushHeaders(); 42 43 // User-specific events 44 const subscription = eventEmitter.on(`user:${userId}`, (data) => { 45 res.write(`data: ${JSON.stringify(data)}\n\n`); 46 }); 47 48 req.on('close', () => { 49 subscription.off(); 50 }); 51});

Pub/Sub Pattern#

1import { EventEmitter } from 'events'; 2 3const eventBus = new EventEmitter(); 4eventBus.setMaxListeners(1000); 5 6// Track connected clients 7const clients = new Map<string, express.Response>(); 8 9// SSE endpoint 10app.get('/events/:channel', (req, res) => { 11 const { channel } = req.params; 12 const clientId = crypto.randomUUID(); 13 14 res.setHeader('Content-Type', 'text/event-stream'); 15 res.flushHeaders(); 16 17 clients.set(clientId, res); 18 19 // Subscribe to channel 20 const handler = (data: any) => { 21 res.write(`data: ${JSON.stringify(data)}\n\n`); 22 }; 23 24 eventBus.on(channel, handler); 25 26 req.on('close', () => { 27 eventBus.off(channel, handler); 28 clients.delete(clientId); 29 }); 30}); 31 32// Publish from anywhere 33function publish(channel: string, data: any) { 34 eventBus.emit(channel, data); 35} 36 37// Usage 38app.post('/notify', (req, res) => { 39 publish('notifications', { 40 type: 'alert', 41 message: req.body.message, 42 }); 43 res.json({ sent: true }); 44});

Scaling with Redis#

1import Redis from 'ioredis'; 2 3const redis = new Redis(); 4const subscriber = new Redis(); 5 6// Subscribe to Redis channel 7subscriber.subscribe('events'); 8 9subscriber.on('message', (channel, message) => { 10 const data = JSON.parse(message); 11 12 // Broadcast to all connected SSE clients 13 clients.forEach((res) => { 14 res.write(`data: ${message}\n\n`); 15 }); 16}); 17 18// Publish from any server instance 19function publishEvent(data: any) { 20 redis.publish('events', JSON.stringify(data)); 21} 22 23// With channel filtering 24const channelClients = new Map<string, Set<express.Response>>(); 25 26subscriber.psubscribe('events:*'); 27 28subscriber.on('pmessage', (pattern, channel, message) => { 29 const channelName = channel.replace('events:', ''); 30 const clients = channelClients.get(channelName); 31 32 clients?.forEach((res) => { 33 res.write(`data: ${message}\n\n`); 34 }); 35});

Next.js Route Handler#

1// app/api/events/route.ts 2export async function GET(request: Request) { 3 const encoder = new TextEncoder(); 4 5 const stream = new ReadableStream({ 6 start(controller) { 7 // Send initial event 8 controller.enqueue( 9 encoder.encode(`data: ${JSON.stringify({ connected: true })}\n\n`) 10 ); 11 12 // Send updates 13 const interval = setInterval(() => { 14 const data = { 15 timestamp: Date.now(), 16 random: Math.random(), 17 }; 18 controller.enqueue( 19 encoder.encode(`data: ${JSON.stringify(data)}\n\n`) 20 ); 21 }, 1000); 22 23 // Handle cancellation 24 request.signal.addEventListener('abort', () => { 25 clearInterval(interval); 26 controller.close(); 27 }); 28 }, 29 }); 30 31 return new Response(stream, { 32 headers: { 33 'Content-Type': 'text/event-stream', 34 'Cache-Control': 'no-cache', 35 'Connection': 'keep-alive', 36 }, 37 }); 38}

SSE vs WebSocket#

SSE: ✓ Simple to implement ✓ Automatic reconnection ✓ HTTP/2 multiplexing ✓ Works through firewalls ✗ Server-to-client only ✗ Limited browser connections (6 per domain) WebSocket: ✓ Bidirectional ✓ Binary data support ✓ Lower latency ✗ More complex ✗ Manual reconnection ✗ May need special proxy config Choose SSE for: - Notifications - Live feeds - Progress updates - Server-initiated updates Choose WebSocket for: - Chat applications - Real-time collaboration - Gaming - Client-initiated events

Best Practices#

Connection: ✓ Set correct headers ✓ Flush headers immediately ✓ Clean up on disconnect ✓ Use heartbeats for keep-alive Scaling: ✓ Use Redis for multi-server ✓ Limit connections per user ✓ Monitor connection count ✓ Handle reconnection gracefully Performance: ✓ Send only necessary data ✓ Batch updates when possible ✓ Use event IDs for resume ✓ Set appropriate retry interval

Conclusion#

Server-Sent Events provide a simple, reliable way to stream data from server to client. They're perfect for notifications, live feeds, and progress updates. Use Redis for scaling across multiple servers, implement proper authentication, and consider WebSockets only when you need bidirectional communication.

Share this article

Help spread the word about Bootspring