Introduction
Server-Sent Events (SSE) is a native web standard for unidirectional real-time communication from server to client. Unlike bidirectional WebSockets, SSE is simpler to implement: no complex frame management, automatic reconnection, and native support in all modern browsers.
Why use it in 2026? For live notifications (new messages, alerts), dashboard updates (real-time stats), or tracking long-running tasks without page reloads. It's lightweight (plain text over HTTP), scalable (persistent connections handled by the browser), and avoids costly polling.
This beginner tutorial guides you step by step: from a basic Node.js server to a robust client with reconnection. By the end, you'll have a fully functional, copy-pasteable example for custom events and JSON data. Estimated time: 15 minutes.
Prerequisites
- Node.js 20+ installed
- Basic JavaScript knowledge (variables, functions, async)
- A code editor (VS Code recommended)
- Modern browser (Chrome, Firefox)
Initialize the project and install dependencies
mkdir sse-tutorial
cd sse-tutorial
npm init -y
npm install express corsThis script creates a project folder, initializes package.json, and installs Express for the HTTP server plus CORS to allow cross-origin requests from the client. Run it in your terminal for a ready setup in 30 seconds.
Understanding SSE Basics
SSE uses a text/event-stream stream over a persistent HTTP connection. The server sends messages in the format data: message\n\n. The client, via EventSource, listens and parses automatically. Key advantage: automatic reconnection after 3 seconds if disconnected.
Create a Basic SSE Server
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.get('/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
let counter = 0;
const interval = setInterval(() => {
counter++;
res.write(`data: Compteur: ${counter}\n\n`);
}, 1000);
req.on('close', () => {
clearInterval(interval);
res.end();
});
});
app.listen(3000, () => {
console.log('Serveur SSE sur http://localhost:3000');
});This server exposes /events as an SSE endpoint. It sends an incrementing counter every second. The required headers enable the persistent stream; req.on('close') cleans up the interval to prevent memory leaks. Run with node server.js.
Create a Simple HTML Client
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>SSE Client</title>
</head>
<body>
<h1>Server-Sent Events Demo</h1>
<div id="messages"></div>
<script>
const eventSource = new EventSource('http://localhost:3000/events');
const messagesDiv = document.getElementById('messages');
eventSource.onmessage = (event) => {
const newMessage = document.createElement('p');
newMessage.textContent = event.data;
messagesDiv.appendChild(newMessage);
};
eventSource.onerror = (error) => {
console.error('Erreur SSE:', error);
};
</script>
</body>
</html>This standalone HTML file connects to /events via EventSource. onmessage displays each received data. onerror logs errors (browser handles auto-reconnection). Open it in a browser to see the live counter.
Test the Basic Demo
- Run
node server.js. - Open
index.htmlin your browser. - Watch messages arrive every second. Close/reopen the tab: auto-reconnection!
Add Custom Events
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.get('/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
let counter = 0;
const interval = setInterval(() => {
counter++;
if (counter % 5 === 0) {
res.write(`event: alerte\ndata: Alerte niveau ${counter}!\n\n`);
} else {
res.write(`data: Compteur normal: ${counter}\n\n`);
}
}, 1000);
req.on('close', () => {
clearInterval(interval);
res.end();
});
});
app.listen(3000, () => {
console.log('Serveur SSE sur http://localhost:3000');
});Upgrade: uses event: name to type messages (e.g., 'alerte'). Every 5 ticks, a special event is sent. This enables client-side filtering. Replace the old server.js and restart.
Advanced Client with Event Handling and JSON
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>SSE Client Avancé</title>
</head>
<body>
<h1>SSE avec Événements Personnalisés</h1>
<div id="normal"></div>
<div id="alertes"></div>
<script>
const eventSource = new EventSource('http://localhost:3000/events');
const normalDiv = document.getElementById('normal');
const alertesDiv = document.getElementById('alertes');
eventSource.addEventListener('alerte', (event) => {
const data = JSON.parse(event.data);
const alertDiv = document.createElement('div');
alertDiv.style.color = 'red';
alertDiv.textContent = `ALERTE: ${data.message}`;
alertesDiv.appendChild(alertDiv);
});
eventSource.onmessage = (event) => {
const p = document.createElement('p');
p.textContent = event.data;
normalDiv.appendChild(p);
};
eventSource.onerror = () => {
console.log('Reconnexion en cours...');
};
</script>
</body>
</html>Client filters events via addEventListener('alerte'). Parses JSON for structured data. onerror gracefully handles disconnections. Replace index.html; now alerts appear in red!
Server with Dynamic JSON Data
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
let usersOnline = 0;
app.get('/events', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
const interval = setInterval(() => {
usersOnline += Math.floor(Math.random() * 3) - 1;
if (usersOnline < 0) usersOnline = 0;
const data = { users: usersOnline, timestamp: Date.now() };
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 2000);
req.on('close', () => {
clearInterval(interval);
res.end();
});
});
app.listen(3000, () => {
console.log('Serveur SSE JSON sur http://localhost:3000');
});Now sends simulated JSON objects (online users). Perfect for real dashboards. The previous client parses them automatically. This shows complex data without changing the SSE protocol.
Best Practices
- Always clean up intervals/timers on
req.closeto avoid leaks. - Limit reconnections:
new EventSource(url, { heartbeatTimeout: 30000 }). - Use IDs for resume:
id: ${timestamp}\non server, auto-handled by client. - Buffer small messages to reduce network latency.
- Scale with Redis for shared state across instances.
Common Errors to Avoid
- Forgetting 'Connection: keep-alive': connection closes after one message.
- Not handling CORS: cross-origin errors block the client.
- Sending without final
\n\n: EventSource can't parse messages. - Ignoring multiple clients: use a broadcaster like ioredis to scale.
Next Steps
- MDN Docs: Server-Sent Events
- Implement with React: use
useEffecthooks for EventSource. - Compare to WebSockets for bidirectional needs.
- Check out our Learni real-time courses: Advanced Node.js, Pro WebSockets.