Building with Server-sent Events
Building real-time applications on the web has never been easier. In this post, I'll explain how you can use server-sent events, or SSE for short, to get real-time data for your web applications.
At the end of this article you should know:
- What a server-sent event is
- How to listen to server-sent events on the browser
- How to send server-sent events from your server
This tutorial is for those who have some familiarity with developing on the web as well as some knowledge in either python or nodejs.
The gist
Server-sent events (SSE) are a client initiated, unidirectional, server controlled messages. When you visit a website that queries an SSE-enabled endpoint, the server can send your browser unlimited amounts of information until you leave that page. SSE urls are always accessed via an asynchronous request from your browser. You can visit a url that serves an SSE endpoint from your browser but there is no standard on what you will experience.
Listening for events
On the front-end, it is quite simple to listen for server-sent events. You just need to write something similar to the code below.
const source = new EventSource('/an-endpoint');
source.onmessage = function logEvents(event) {
console.log(JSON.parse(data));
}
// @language-override:React Hooks
import React, { useState, useEffect } from 'react';
const useEventSource = (url) => {
const [data, updateData] = useState(null);
useEffect(() => {
const source = new EventSource(url);
source.onmessage = function logEvents(event) {
updateData(JSON.parse(event.data));
}
}, []);
return data
}
In this code snippet, I create a new EventSource
object that listens on the url /an-endpoint
. EventSource
is a helper class that does the heavy lifting of listening to server-sent-events for us. All we need to do now is to attach a function, in this case logEvents
, to the onmessage
handler.
Any time our server sends us a message, source.onmessage
will be fired.
Let's look at a more realistic example. The code below listens on a server at the url https://ds.shub.dev/e/temperatures
. Every 5 seconds, the server returns a server-sent event with the temperature of my living room.
// @codepen-link:https://codepen.io/4shub/pen/qBOvYYM
const source = new EventSource('https://ds.shub.dev/e/temperatures');
source.onmessage = function showWeather(event) {
// event.data will always return a string of our event response
// in this case, our server sends us a stringified JSON object
// that we can parse below
const data = JSON.parse(event.data);
const temperatureOutputEl = document.getElementById('temperature-output');
const updatedAtOutputEl = document.getElementById('updated-at');
temperatureOutputEl.innerText = data.temperature;
updatedAtOutputEl.innerText = data.updatedAt;
}
// @language-override:React Hooks
// @codepen-link:https://codepen.io/4shub/pen/QWjorRp
import React, { useState, useEffect } from 'react';
import { render } from "react-dom";
const useEventSource = (url) => {
const [data, updateData] = useState(null);
useEffect(() => {
const source = new EventSource(url);
source.onmessage = function logEvents(event) {
updateData(JSON.parse(event.data));
}
}, [])
return data;
}
function App() {
const data = useEventSource('https://ds.shub.dev/e/temperatures');
if (!data) {
return <div />;
}
return <div>The current temperature in my living room is {data.temperature} as of {data.updatedAt}</div>;
}
render(<App />, document.getElementById("root"));
<div>
The current temperature in my living room is
<span id="temperature-output"></span> as of <span id="updated-at">--</span>
</div>
What's happening behind the scenes?

Let's look at these two properties of EventSource:
url
- The url that we want to listen on for changesreadyState
- The state of the connection. This can be(0) CONNECTING
,(1) OPEN
and(2) CLOSED
. Initially this value isCONNECTING
.
When EventSource is invoked, the browser creates a request with the header Accept: text/event-stream
to the url
that was passed through.
The browser will then verify if the request returns a 200 OK
response and a header containing Content-Type
: text/event-stream
. If successful, our readyState
will be set to OPEN
and trigger the method onopen
.
The data from that response will then be parsed and an event will be fired that triggers onmessage
.
Finally, the server we pinged can send us an unlimited amount of event-stream
content until:
- We close our page
- We fire the
close()
method on event source - The server sends us an invalid response
When we finally close our connection, the EventSource
object's readyState
will fire a task that sets readyState
to CLOSED
and trigger the onclose
event.
In case of a network interruption, the browser will try to reconnect until the effort is deemed "futile," as determined by the browser (unfortunately, there are no standards on what constitutes "futile").
Sending events on the server
Sending server-sent events is just as easy as listening to them. Below, I've written a few different implementations of sending server-sent events to your client.
// @language-override: Node + Express
// @repl-it-link:https://repl.it/@4shub/server-sent-events-node
const express = require('express');
const server = express();
const port = 3000;
// create helper middleware so we can reuse server-sent events
const useServerSentEventsMiddleware = (req, res, next) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
// only if you want anyone to access this endpoint
res.setHeader('Access-Control-Allow-Origin', '*');
res.flushHeaders();
const sendEventStreamData = (data) => {
const sseFormattedResponse = `data: ${JSON.stringify(data)}\n\n`;
res.write(sseFormattedResponse);
}
// we are attaching sendEventStreamData to res, so we can use it later
Object.assign(res, {
sendEventStreamData
});
next();
}
const streamRandomNumbers = (req, res) => {
// We are sending anyone who connects to /stream-random-numbers
// a random number that's encapsulated in an object
let interval = setInterval(function generateAndSendRandomNumber(){
const data = {
value: Math.random(),
};
res.sendEventStreamData(data);
}, 1000);
// close
res.on('close', () => {
clearInterval(interval);
res.end();
});
}
server.get('/stream-random-numbers', useServerSentEventsMiddleware,
streamRandomNumbers)
server.listen(port, () => console.log(`Example app listening at
http://localhost:${port}`));
# @language-override: Python + Flask
# @repl-it-link:https://repl.it/@4shub/server-sent-events-python
from flask import Flask, Response
from random import seed
from random import randint
import json
import time
app = Flask(__name__)
# for random number generation
seed(1)
def prepare_data(payload):
data = json.dumps(payload)
return 'data: %s\n\n' % data
def generate_random_number():
return { 'value': randint(1, 10) }
def generate_response_payload():
# we want to send our events forever
while True:
payload = generate_random_number()
# we use a generator here to pass data to the client
# every time the interpreter passes this yield
# it will pass back the latest payload
# if we used return instead we would only run return data once
yield prepare_data(payload)
# we want to wait a second so we aren't spamming the user
# this can be used to throttle returned data
# if data is throttled elsewhere, this can be removed
time.sleep(1)
@app.route("/stream-random-numbers")
def stream():
stream_response = Response(generate_response_payload(), mimetype="text/event-stream")
# optional: only if you want others to access your application
stream_response.headers.add('Access-Control-Allow-Origin', '*')
return stream_response
if __name__ == "__main__":
# Flask needs to run in threaded mode
app.run(host='0.0.0.0', port=3000, threaded=True)
In the example above, I created a server with an event-stream that sends users a random number every second.
Conclusion
Many companies use server-sent events to pipe data to their users in real time. LinkedIn uses server sent events for their messaging service, Mapbox uses SSE to display live map data, and many analytics tools use SSE to show real-time user reports. SSE will only become more prominent as monitoring tools and real-time events become more relevant to users. Let me know if you try it out — I'd love to see what you come up with!
--
Written by Shubham Naik on May 23, 2020.
Comments, Questions, or Clarifications? Contact shub@shub.club