In this article, we’ll explore how to use server-sent events to enable a client to receive automatic updates from a server via an HTTP connection. We’ll also look at why this is useful, and we’ll show practical demonstrations of how to use server-sent events with Node.js.
- Why Server-sent Events Are Useful
- Server-sent Events Quick Start
- Advanced Server-sent Events
- Conclusion
Why Server-sent Events Are Useful
The Web is based on request-response HTTP messages. Your browser makes a URL request and a server responds with data. That may lead to further browser requests and server responses for images, CSS, JavaScript etc. The server cannot initiate messages to the browser, so how can it indicate that data has changed? Fortunately, you can add features such as live news bulletins, weather reports, and stock prices with server-sent events.
Implementing live data updates using standard web technologies has always been possible:
- The 1990s Web used a full-page or frame/iframe refresh.
- The 2000s Web introduced Ajax, which could use long polling to request data and update the appropriate DOM elements with new information.
Neither option is ideal, since the browser must trigger a refresh. If it makes requests too often, no data will have changed so both the browser and server do unnecessary work. If it makes requests too slowly, it may miss an important update and the stock price you’re watching has already crashed!
Server-sent events (SSE) allow a server to push data to the browser at any time:
- The browser still makes the initial request to establish a connection.
- The server returns an event-stream response and keeps the connection open.
- The server can use this connection to send text messages at any point.
- The incoming data raises a JavaScript event in the browser. An event handler function can parse the data and update the DOM.
In essence, SSE is an unending stream of data. Think of it as downloading an infinitely large file in small chunks that you can intercept and read.
SSE was first implemented in 2006 and all major browsers support the standard. It’s possibly less well known than WebSockets, but server-sent events are simpler, use standard HTTP, support one-way communication, and offer automatic reconnection. This tutorial provides example Node.js code without third-party modules, but SSE is available in other server-side languages including PHP.
Server-sent Events Quick Start
The following demonstration implements a Node.js web server which outputs a random number between 1 and 1,000 at a random interval of at least once every three seconds.
You can find our Node.js SSE demonstration here.
The code uses the standard Node.js http
and url
modules for creating a web server and parsing URLs:
import http from "node:http";
import url from "node:url";
The server examines the incoming URL request and reacts when it encounters a /random
path:
const port = 8000;
http.createServer(async (req, res) => {
// get URI path
const uri = url.parse(req.url).pathname;
// return response
switch (uri) {
case "/random":
sseStart(res);
sseRandom(res);
break;
}
}).listen(port);
console.log(`server running: http://localhost:${port}\n\n`);
It initially responds with the SSE HTTP event-stream header:
// SSE head
function sseStart(res) {
res.writeHead(200, {
Content-Type: "text/event-stream",
Cache-Control: "no-cache",
Connection: "keep-alive"
});
}
Another function then sends a random number and calls itself after a random interval has elapsed:
// SSE random number
function sseRandom(res) {
res.write("data: " + (Math.floor(Math.random() * 1000) + 1) + "\n\n");
setTimeout(() => sseRandom(res), Math.random() * 3000);
}
If you run the code locally, you can test the response using cURL in your terminal:
$> curl -H Accept:text/event-stream http://localhost:8000/random
data: 481
data: 127
data: 975
Press Ctrl | Cmd and C to terminate the request.
The browser’s client-side JavaScript connects to the /random
URI using an EventSource object constructor:
// client-side JS
const source = new EventSource("/random");
Incoming data triggers a message
event handler where the string following data:
is available in the event object’s .data
property:
source.addEventListener('message', e => {
console.log('RECEIVED', e.data);
});
Important notes
- Like Fetch(), the browser makes a standard HTTP request, so you may need to handle CSP, CORS and optionally pass a second
{ withCredentials: true }
argument to theEventSource
constructor to send cookies. - The server must retain individual
res
response objects for every connected user to send them data. It’s achieved in the code above by passing the value in a closure to the next call. - Message data can only be a string (perhaps JSON) sent in the format
data: <message>\n\n
. The terminating carriage returns are essential. - The server can terminate an SSE response at any time with
res.end()
, but… - When a disconnect occurs, the browser automatically attempts to reconnect; there’s no need to write your own reconnection code.
Advanced Server-sent Events
SSE requires no more code than that shown above, but the following sections discuss further options.
One vs many SSE channels
A server could provide any number of SSE channel URLs. For example:
/latest/news
/latest/weather
/latest/stockprice
This may be practical if a single page shows one topic, but less so if a single page shows news, weather, and stock prices. In that situation, the server must maintain three connections for each user, which could lead to memory problems as traffic increases.
An alternative option is to provide a single endpoint URL, such as /latest
, which sends any data type on one communication channel. The browser could indicate the topics of interest in the URL query string — for example, /latest?type=news,weather,stockprice
— so the server can limit SSE responses to specific messages.
Sending different data on a single channel
Messages from the server can have an associated event:
passed on the line above the data:
to identify specific types of information:
event: news
data: SSE is great!
event: weather
data: { "temperature": "20C", "wind": "10Kph", "rain": "25%" }
event: stock
data: { "symbol": "AC", "company": "Acme Corp", "price": 123.45, "increase": -1.1 }
These will not trigger the client-side "message"
event handler. You must add handlers for each type of event
. For example:
// news message handler
source.addEventListener('news', e => {
document.getElementById('headline')
.textContent = e.data;
});
// weather message handler
source.addEventListener('weather', e => {
const w = JSON.parse(e.data);
document.getElementById('weather')
.textContent = `${ w.temperature } with ${ w.wind } wind`;
});
// stock message handler
source.addEventListener('stock', e => {
const s = JSON.parse(e.data);
document.getElementById(`stock-${ s.symbol }`)
.textContent = `${ s.share }: ${ s.price } (${ s.increase }%)`;
});
Using data identifiers
Optionally, the server can also send an id:
after a data:
line:
event: news
data: SSE is great!
id: 42
If the connection drops, the browser sends the last id
back to the server in the Last-Event-ID
HTTP header so the server can resend any missed messages.
The most recent ID is also available client-side in the event object’s .lastEventId
property:
// news message handler
source.addEventListener('news', e => {
console.log(`last ID: ${ e.lastEventId }`);
document.getElementById('headline')
.textContent = e.data;
});
Specifying retry delays
Although reconnection is automatic, your server may know that new data is not expected for a specific period, so there’s no need to retain an active communication channel. The server can send a retry:
response with a milliseconds value either on its own or as part of a final message. For example:
retry: 60000
data: Please don't reconnect for another minute!
On receipt, the browser will drop the SSE connection and attempt to reconnect after the delay period has elapsed.
Other event handlers
As well as "message"
and named events, you can also create "open"
and "error"
handlers in your client-side JavaScript.
An "open"
event triggers when the server connection is established. It could be used to run additional configuration code or initialize DOM elements:
const source = new EventSource('/sse1');
source.addEventListener('open', e => {
console.log('SSE connection established.');
});
An "error"
event triggers when the server connection fails or terminates. You can examine the event object’s .eventPhase
property to check what happened:
source.addEventListener('error', e => {
if (e.eventPhase === EventSource.CLOSED) {
console.log('SSE connection closed');
}
else {
console.log('error', e);
}
});
Remember, there’s no need to reconnect: it occurs automatically.
Terminating SSE communication
The browser can terminate an SSE communication using the EventSource object’s .close()
method. For example:
const source = new EventSource('/sse1');
// close after one hour
setTimeout(() => source.close(), 3_600_000);
The server can terminate the connection by:
- firing
res.end()
or sending aretry:
delay, then - returning an HTTP status 204 when the same browser attempts to reconnect.
Only the browser can re-establish a connection by creating a new EventSource
object.
Conclusion
Server Side Events provide a way to implement live page updates which are possibly easier, more practical, and more lightweight than Fetch()
-based Ajax polling. The complexity is at the server end. You must:
- maintain all user’s active connections in memory, and
- trigger data transmissions when something changes.
But this is fully under your control, and scaling should be no more complex than any other web application.
The only drawback is that SSE doesn’t allow you to send messages from the browser to the server (apart from the initial connection request). You could use Ajax, but that’s too slow for apps such as action games. For proper two-way communication, you require WebSockets. Check out How to Use WebSockets in Node.js to Create Real-time Apps to learn more!
Craig is a freelance UK web consultant who built his first page for IE2.0 in 1995. Since that time he's been advocating standards, accessibility, and best-practice HTML5 techniques. He's created enterprise specifications, websites and online applications for companies and organisations including the UK Parliament, the European Parliament, the Department of Energy & Climate Change, Microsoft, and more. He's written more than 1,000 articles for SitePoint and you can find him @craigbuckler.