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.