javascript
46 lines · 8 steps
Server-Sent Events with Express
An Express route streams live notifications to a browser over a single long-lived connection using Server-Sent Events.
Explained by
highlit
1const express = require('express');
2const EventEmitter = require('events');
3
4const router = express.Router();
5const notifications = new EventEmitter();
6notifications.setMaxListeners(0);
7
8router.get('/notifications/stream', (req, res) => {
9 res.writeHead(200, {
10 'Content-Type': 'text/event-stream',
11 'Cache-Control': 'no-cache, no-transform',
12 Connection: 'keep-alive',
13 'X-Accel-Buffering': 'no',
14 });
15 res.flushHeaders();
16
17 const userId = req.user.id;
18 res.write(`retry: 5000\n\n`);
19
20 const send = (payload) => {
21 res.write(`event: notification\n`);
22 res.write(`id: ${payload.id}\n`);
23 res.write(`data: ${JSON.stringify(payload)}\n\n`);
24 };
25
26 const onNotify = (payload) => {
27 if (payload.userId === userId) send(payload);
28 };
29 notifications.on('push', onNotify);
30
31 const heartbeat = setInterval(() => {
32 res.write(`: keep-alive ${Date.now()}\n\n`);
33 }, 15000);
34
35 req.on('close', () => {
36 clearInterval(heartbeat);
37 notifications.off('push', onNotify);
38 res.end();
39 });
40});
41
42function publish(userId, data) {
43 notifications.emit('push', { id: Date.now(), userId, ...data });
44}
45
46module.exports = { router, publish };
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1SSE keeps one HTTP response open and writes framed text events instead of closing after a single body.
- 2A shared EventEmitter acts as a lightweight pub/sub hub so any part of the app can push to connected clients.
- 3Every per-connection resource — listeners, intervals, the response — must be torn down on the client's close event to avoid leaks.
Related explainers
python
import json import time import queue
Server-Sent Events streaming in Flask
server-sent-events
streaming
pub-sub
Advanced
9 steps
java
public List<OrderSummary> streamRecentOrders(LocalDateTime since, Consumer<OrderSummary> handler) { String sql = """ SELECT id, customer_id, total_cents, status, created_at FROM orders
Streaming large JDBC result sets safely
jdbc
streaming
resource-management
Intermediate
7 steps
javascript
import { useState, useEffect, useCallback } from 'react'; export function useCountdown(initialSeconds) { const [secondsLeft, setSecondsLeft] = useState(initialSeconds);
Building a useCountdown hook in React
custom-hooks
state-management
side-effects
Intermediate
8 steps
javascript
import { useState, useRef } from "react"; export function TagInput({ initialTags = [], onChange }) { const [tags, setTags] = useState(initialTags);
Building a tag input in React
controlled-inputs
state-management
keyboard-handling
Intermediate
8 steps
javascript
import { NavLink, useLocation } from 'react-router-dom'; const NAV_ITEMS = [ { to: '/', label: 'Dashboard', end: true },
Building an accessible Sidebar in React
routing
accessibility
declarative-ui
Intermediate
6 steps
javascript
function escapeHtml(str) { return str.replace(/[&<>"']/g, (ch) => ({ '&': '&', '<': '<',
Safely highlighting search matches in text
html-escaping
regex
search-highlighting
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/server-sent-events-with-express-explained-javascript-59ec/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.