WebSockets & Real-Time Apps

Server-Sent Events (SSE)

16 min Lesson 18 of 35

Server-Sent Events (SSE)

Server-Sent Events (SSE) is a standard allowing servers to push data to web clients over HTTP. Unlike WebSockets, SSE is unidirectional (server to client only) and works over traditional HTTP connections.

SSE vs WebSockets

Understanding when to use each technology:

Feature SSE WebSockets
Direction Server to client only Bidirectional
Protocol HTTP WebSocket protocol
Reconnection Automatic Manual
Data Format Text only (UTF-8) Text and binary
Browser Support All modern browsers All modern browsers
Complexity Simple More complex
When to Use SSE: News feeds, stock tickers, notifications, live scores, server monitoring dashboards - any scenario where the server needs to push updates but clients don't need to send frequent messages back.

EventSource API

The client-side API for SSE is straightforward:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>SSE Example</title> </head> <body> <div id="notifications"></div> <script> // Create EventSource connection const eventSource = new EventSource('/api/events'); // Listen for messages eventSource.onmessage = (event) => { console.log('New message:', event.data); displayNotification(event.data); }; // Listen for connection open eventSource.onopen = () => { console.log('Connection opened'); }; // Listen for errors eventSource.onerror = (error) => { console.error('EventSource error:', error); if (eventSource.readyState === EventSource.CLOSED) { console.log('Connection closed'); } }; function displayNotification(message) { const div = document.createElement('div'); div.textContent = message; document.getElementById('notifications').appendChild(div); } // Close connection when needed // eventSource.close(); </script> </body> </html>

Creating SSE Endpoint in Express

Server-side implementation with Node.js and Express:

const express = require('express'); const app = express(); // SSE endpoint app.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'); // Optional: Enable CORS res.setHeader('Access-Control-Allow-Origin', '*'); // Send initial comment (helps keep connection alive) res.write(':ok\n\n'); // Send a message every 5 seconds const intervalId = setInterval(() => { const data = { timestamp: new Date().toISOString(), message: 'Hello from server' }; // Format: data: JSON\n\n res.write(`data: ${JSON.stringify(data)}\n\n`); }, 5000); // Clean up on client disconnect req.on('close', () => { clearInterval(intervalId); console.log('Client disconnected'); }); }); app.listen(3000, () => { console.log('SSE server running on port 3000'); });
Note: The SSE message format is strict: data: message\n\n. The double newline (\n\n) indicates the end of a message.

Sending Events with Event Types

SSE supports custom event types for different message categories:

// Server-side: Send different event types app.get('/api/events', (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // Send notification event function sendNotification(message) { res.write(`event: notification\n`); res.write(`data: ${JSON.stringify(message)}\n\n`); } // Send update event function sendUpdate(data) { res.write(`event: update\n`); res.write(`data: ${JSON.stringify(data)}\n\n`); } // Send alert event function sendAlert(alert) { res.write(`event: alert\n`); res.write(`data: ${JSON.stringify(alert)}\n\n`); } // Example: Send different events setInterval(() => { sendNotification({ text: 'New comment on your post' }); }, 10000); setInterval(() => { sendUpdate({ users: 42, requests: 1523 }); }, 5000); req.on('close', () => { console.log('Client disconnected'); }); });
// Client-side: Listen to specific event types const eventSource = new EventSource('/api/events'); // Listen for notification events eventSource.addEventListener('notification', (event) => { const data = JSON.parse(event.data); console.log('Notification:', data.text); showNotification(data.text); }); // Listen for update events eventSource.addEventListener('update', (event) => { const data = JSON.parse(event.data); console.log('Update:', data); updateDashboard(data); }); // Listen for alert events eventSource.addEventListener('alert', (event) => { const data = JSON.parse(event.data); console.log('Alert:', data); showAlert(data); }); // Generic message handler (for events without type) eventSource.onmessage = (event) => { console.log('Generic message:', event.data); };

Automatic Reconnection

SSE automatically reconnects when the connection drops:

// Server: Send retry interval (in milliseconds) app.get('/api/events', (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // Set retry interval to 3 seconds res.write('retry: 3000\n\n'); // Send events... });
// Client: Handle reconnection const eventSource = new EventSource('/api/events'); let reconnectAttempts = 0; const maxReconnectAttempts = 5; eventSource.onerror = (error) => { if (eventSource.readyState === EventSource.CONNECTING) { reconnectAttempts++; console.log(`Reconnecting... (attempt ${reconnectAttempts})`); if (reconnectAttempts >= maxReconnectAttempts) { console.error('Max reconnection attempts reached'); eventSource.close(); // Show user notification alert('Lost connection to server. Please refresh.'); } } else if (eventSource.readyState === EventSource.CLOSED) { console.log('Connection closed'); } }; eventSource.onopen = () => { reconnectAttempts = 0; console.log('Connection (re)established'); };

Last Event ID for Resuming

Use event IDs to resume from the last received event after reconnection:

// Server: Send events with IDs let eventId = 0; app.get('/api/events', (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // Get last event ID from client (if reconnecting) const lastEventId = req.headers['last-event-id']; console.log('Last event ID received:', lastEventId); // Send events with IDs const intervalId = setInterval(() => { eventId++; res.write(`id: ${eventId}\n`); res.write(`data: ${JSON.stringify({ id: eventId, message: 'Event ' + eventId })}\n\n`); }, 2000); req.on('close', () => { clearInterval(intervalId); }); });
// Client: EventSource automatically sends last event ID on reconnect const eventSource = new EventSource('/api/events'); eventSource.onmessage = (event) => { console.log('Event ID:', event.lastEventId); console.log('Data:', event.data); // Store last event ID if needed localStorage.setItem('lastEventId', event.lastEventId); };
Best Practice: Always send event IDs for critical data streams. This allows clients to resume from the last received event after a disconnection, preventing data loss.

SSE Use Cases

Ideal scenarios for Server-Sent Events:

  • Real-time notifications: User notifications, alerts, system messages
  • Live feeds: News updates, social media feeds, activity streams
  • Monitoring dashboards: Server metrics, application logs, analytics
  • Stock tickers: Live stock prices, cryptocurrency rates
  • Sports scores: Live game updates, score changes
  • Progress tracking: File uploads, long-running tasks
  • IoT data streams: Sensor readings, device status updates

Real-World Example: Live Notifications

// Server: Notification system const express = require('express'); const app = express(); // Store active connections const clients = new Map(); // SSE endpoint app.get('/api/notifications', (req, res) => { const userId = req.query.userId; res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // Add client to active connections clients.set(userId, res); // Send initial connection confirmation res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`); // Keep-alive ping every 30 seconds const keepAliveId = setInterval(() => { res.write(':ping\n\n'); }, 30000); req.on('close', () => { clearInterval(keepAliveId); clients.delete(userId); console.log(`User ${userId} disconnected`); }); }); // API endpoint to send notification app.post('/api/send-notification', express.json(), (req, res) => { const { userId, notification } = req.body; const client = clients.get(userId); if (client) { client.write(`event: notification\n`); client.write(`data: ${JSON.stringify(notification)}\n\n`); res.json({ success: true }); } else { res.status(404).json({ error: 'User not connected' }); } }); app.listen(3000);
// Client: Notification UI const userId = 'user123'; const eventSource = new EventSource(`/api/notifications?userId=${userId}`); eventSource.addEventListener('notification', (event) => { const notification = JSON.parse(event.data); displayNotification(notification); }); function displayNotification(notification) { // Show browser notification if (Notification.permission === 'granted') { new Notification(notification.title, { body: notification.message, icon: notification.icon }); } // Show in-app notification const notifDiv = document.createElement('div'); notifDiv.className = 'notification'; notifDiv.innerHTML = ` <strong>${notification.title}</strong> <p>${notification.message}</p> `; document.getElementById('notifications').appendChild(notifDiv); // Auto-dismiss after 5 seconds setTimeout(() => notifDiv.remove(), 5000); }

SSE Limitations

Important Limitations:
  • Browser connection limit: Most browsers limit SSE connections to 6 per domain
  • No binary data: SSE only supports UTF-8 text (WebSockets support binary)
  • One-way only: Client cannot send data through SSE connection
  • No mobile background: Mobile browsers may close connections when app is backgrounded
  • HTTP/1.1 limitation: Each SSE connection uses one HTTP connection (better with HTTP/2)

Exercise: Build a Live Blog

Create a Real-Time Blog with SSE:
  1. Build an Express server with SSE endpoint for blog posts
  2. Create a client page that displays blog posts as they are published
  3. Add different event types: 'post' for new posts, 'update' for edits, 'delete' for removals
  4. Implement automatic reconnection with visual indicator
  5. Use event IDs to handle reconnection gracefully
  6. Add a simple admin interface to publish posts
  7. Bonus: Show real-time view count for each post

Summary

  • SSE provides simple server-to-client real-time communication over HTTP
  • Use EventSource API on the client side - it's built into all modern browsers
  • SSE automatically reconnects when connections drop
  • Event types allow organizing different message categories
  • Event IDs enable resuming from last received message
  • SSE is ideal for notifications, feeds, and monitoring dashboards
  • Choose SSE over WebSockets when you only need server-to-client communication
  • Be aware of browser connection limits (6 per domain)