Only show guides for your app
When the server just needs to push updates one way — live scores, progress bars, notifications — SSE is simpler than WebSockets: a plain HTTP stream the browser reconnects for you. Verified with curl -N.
Not every live feature needs a two-way socket. When updates only flow one way — server to browser — Server-Sent Events (SSE) are the simplest tool for the job: an ordinary HTTP response that the server keeps open and writes to over time. The browser has built-in support (EventSource) that even reconnects automatically. This guide builds a working SSE endpoint on your one Falix port.
| At a glance | |
|---|---|
| You need | A server running the Node.js application |
| Background | Build an Express website + API |
| Plan | Any plan |
| Time | Twenty minutes |
| SSE | WebSockets / Socket.IO | |
|---|---|---|
| Direction | Server → browser only | Both ways |
| Protocol | Plain HTTP (a long response) | A protocol upgrade |
| Browser client | Built-in EventSource |
WebSocket / a library |
| Auto-reconnect | Built in, free | You (or Socket.IO) add it |
| Best for | Live scores, progress, feeds, notifications | Chat, games, collaboration |
If the browser never needs to send real-time messages back — it just wants to be told when something changes — SSE gives you live updates with a fraction of the moving parts. When you need two-way traffic, reach for WebSockets or Socket.IO.
SSE needs no library — it's a normal Express route with the right headers and a response you don't end. This one streams a numbered "tick" every second:
const express = require('express');
const app = express();
app.get('/events', (req, res) => {
res.set({
'Content-Type': 'text/event-stream', // the SSE content type
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
res.flushHeaders(); // send headers now; keep the body open
// If the browser reconnects, it sends the last id it saw.
let id = Number(req.headers['last-event-id']) || 0;
const timer = setInterval(() => {
id++;
res.write(`id: ${id}\n`);
res.write(`event: tick\n`);
res.write(`data: ${JSON.stringify({ id, time: new Date().toISOString() })}\n\n`);
}, 1000);
req.on('close', () => clearInterval(timer)); // stop when the client leaves
});
const PORT = process.env.SERVER_PORT || 8080;
app.listen(PORT, '0.0.0.0', () => console.log(`Listening on port ${PORT}`));
The whole protocol is that text format. Each message is a block of field: value lines ending in a blank line:
data: — the payload (the only required field). Send JSON as a string and parse it on the client.event: — an optional name, so the client can listen for tick specifically.id: — an optional id the browser remembers and sends back as Last-Event-ID when it reconnects, so you can resume.The req.on('close', ...) cleanup is essential: without it, the interval keeps running for every client that ever connected, and the timers pile up.
You can watch the raw stream with curl -N (the -N disables buffering so events appear live):
curl -N http://YOUR_ADDRESS:PORT/events
Events tick in once a second, exactly as written:
id: 1
event: tick
data: {"id":1,"time":"2026-07-18T03:36:55.875Z"}
id: 2
event: tick
data: {"id":2,"time":"2026-07-18T03:36:56.876Z"}
To prove the resume path, send a Last-Event-ID header and the server picks up right after it:
curl -N -H "Last-Event-ID: 42" http://YOUR_ADDRESS:PORT/events
# -> id: 43, id: 44, ...
That's the whole reconnection story, and the browser does it for you automatically.
EventSource is built into every browser — no library, no reconnect code:
const source = new EventSource('/events');
source.addEventListener('tick', (e) => {
const data = JSON.parse(e.data);
console.log('tick', data.id, data.time);
});
source.onerror = () => {
// The browser is ALREADY reconnecting. Usually there's nothing to do here.
};
🎯 Good to know: When the connection drops,
EventSourcereconnects on its own and sends the lastid:it received as theLast-Event-IDheader — the exact header the server reads above. Between the two, a phone changing networks or a brief hiccup heals with zero code from you. That built-in reconnect is SSE's best feature.
The ticking counter is a stand-in. In a real app you keep the open res objects in a list and write to them whenever something happens — a new order, a finished job, a chat post from elsewhere:
const clients = new Set();
app.get('/events', (req, res) => {
res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' });
res.flushHeaders();
clients.add(res);
req.on('close', () => clients.delete(res));
});
// Call this from anywhere to push to every connected browser:
function broadcast(event, data) {
for (const res of clients) {
res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
}
That's a live notifications feed in a dozen lines.
fetch/POST) — a "like" button is a plain POST, and the result comes back down the SSE feed. If you need true two-way, use WebSockets.Cache-Control: no-cache header above helps). See Domains and HTTPS.The EventSource API and the event-stream format are web standards — MDN's Server-sent events guide is the canonical reference.
curl hangs with no output — that's SSE working (an open stream). Add -N so events print as they arrive instead of buffering.Content-Type: text/event-stream and Cache-Control: no-cache, and that you call res.flushHeaders().req.on('close', ...) to clear intervals and drop the client.