Real-time data streaming using FastAPI and WebSockets
We have several options for real-time data streaming in web applications. We can use polling, long-polling, Server-Sent Events and WebSockets. The last two can be used for server-push scenarios where we want to send data to a browser without any specific request from the client. All of this solutions have their advantages and disadvantages, so we need to make sure that a particular approach is the best for our application.
Today, we will have a look at how simple it can be to start streaming data to a browser from a backend Python application using WebSockets. There are multiple Python web frameworks capable of doing that, but for this demostration we will use FastAPI, a modern async framework that is gaining momentum in the new space of Python async frameworks.
Streaming in WebSockets basically means sending small data packets over an open connection between the server and the client. We can send both text or binary data packets and what we put inside is completely up to us. Since JSON is a popular data format (although not really memory efficient), we will use it to structure our data packets and send them as text for easy debugging.
The example data will be just a series of numbers that we will render into a self-updating chart on the client web page, simulating time-series data. For this purpose, I have chosen a simple JavaScript library TimeChart since it is easy to use, built on WebGL for performance and supports continuous updates for time-series data (the data in the chart will automatically flow from right to left, always displaying new data points in the chart while hiding the old ones). There are other charting libraries that can do this, but TimeChart is very minimalistic and has small footprint, perfect for this example.
To run the program we will need to install a couple of dependencies: FastAPI (the web framework), Uvicorn (ASGI server) and jinja2 (to render server-side templates) for the backend and TimeChart and its dependencies for the frontend. As always you can find the whole example on Github as Python real-time data streaming using FastAPI and WebSockets, which includes all the source code as well as dependencies defined using Poetry.
Let’s first start with our Python code:
import json
import asyncio
from fastapi import FastAPI
from fastapi import Request
from fastapi import WebSocket
from fastapi.templating import Jinja2Templates
app = FastAPI()
templates = Jinja2Templates(directory="templates")
with open('measurements.json', 'r') as file:
measurements = iter(json.loads(file.read()))
@app.get("/")
def read_root(request: Request):
return templates.TemplateResponse("index.htm", {"request": request})
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
await asyncio.sleep(0.1)
payload = next(measurements)
await websocket.send_json(payload)
As you can see, the code is pretty short! We are basically doing a couple of things here:
- creating FastAPI
app
object that we will later run using uvicorn server - loading our measurements JSON file that contains a sample dataset of values we will be streaming to the client
- defining our homepage route returning a rendered jinja2 template which holds our client-side JavaScript application
- defining our WebSocket endpoint (on
/ws
) that will continuously send our values one by one as JSON data structure to any client that will connect to it
I used iter()
function when loading our sample dataset to create a Python iterator so that we can simply grab a next value in the list, giving us the illusion of having a continuous data stream.
Also, asyncio.sleep()
is called to make the data stream a bit slower.
Now, let’s have a look at our index.htm
template, where we also store our JavaScript code:
<html>
<head>
<title>Real time streaming</title>
<script src="https://d3js.org/d3-array.v2.min.js"></script>
<script src="https://d3js.org/d3-color.v1.min.js"></script>
<script src="https://d3js.org/d3-format.v1.min.js"></script>
<script src="https://d3js.org/d3-interpolate.v1.min.js"></script>
<script src="https://d3js.org/d3-time.v1.min.js"></script>
<script src="https://d3js.org/d3-time-format.v2.min.js"></script>
<script src="https://d3js.org/d3-scale.v3.min.js"></script>
<script src="https://d3js.org/d3-selection.v1.min.js"></script>
<script src="https://d3js.org/d3-axis.v1.min.js"></script>
<script src="https://huww98.github.io/TimeChart/dist/timechart.min.js"></script>
<style>
#chart { width: 100%; height: 300px; margin-top: 300px; }
</style>
</head>
<body>
<div id="chart"></div>
<script>
const el = document.getElementById('chart');
const dataPoints = [];
const chart = new TimeChart(el, {
series: [{ data: dataPoints, name: 'Real-time measurement streaming', color: 'darkblue' }],
realTime: true,
xRange: { min: 0, max: 500 },
});
const ws = new WebSocket("ws://localhost:8000/ws");
let x = 0;
ws.onmessage = function(event) {
const measurement = JSON.parse(event.data);
x += 1
dataPoints.push({x, y: measurement.value});
chart.update();
};
</script>
</body>
</html>
At the very beginning we just need to add some boiler plate: link to the TimeChart library and all its dependencies, style our chart placeholder and create our HTML chart placeholder (as a simple div with the id chart
).
The rest of the code is our little JavaScript application where we create a TimeChart object representing our chart, create a WebSocket object and define a listener that will receive our data stream (the ws.onmessage
function).
There are some things to think of when using TimeChart library:
- the
data
should hold an array of objects in the format ofx: , y:
for x and y values - this array can be updated any time, but once updated we need to call
chart.update()
so that the changes are reflected in the chart realTime: true
is telling TimeChart to update the visible range when new data is addedxRange
defines the visible range for X variable (it makes sense to choose it according to the browser width or the container width to stretch the data in the way it will be nice for the eyes)- we are simply using simple +1 increment on the X axis, but when developing a real time-series chart, we would need to consider what values to put here (TimeChart requires an incrementing sequence that would be ideally based on the time data of the events)
Consuming a WebSockets data stream is also simple on the JavaScript side. We just need to specify the address of the stream (which is our ws endpoint) and define what should happen when we receive data. As we are consuming the stream as text, we just need to convert each data frame to JSON to grab our value.
Believe it or not, this is it! We have just created a real-time data stream of fake time-series data and displayed it as a dynamic self-updating chart in the browser!