What are Server Sent Events (SSE)?
Server Sent Events (SSE) are a super lightweight method of sending messages to frontend. Let's say you have to wait for some long processing of files, chit-chat between your AI Agents, the user will stare at a spinning wheel, get bored then cancel his subscription.
Django is great, but not at async stuff, speed and DX. SSE needs an async web server to run - something like daphne or uvicorn.
Current solutions for implementing SSE in Django
I tried django-eventstream + daphne but hot-reload for server and browser broke. Then I tried a custom implementation of SSE + uvicorn - same hot-reload didn't worked (I used --reload flag).
I wasted a few hours saving 5 seconds of restarting server and refreshing browser on each change to create a solution.
New way of implementing SSE in Django with WSGI
The solution is a Golang service which listens to messages published by Django service and sends those messages to frontend. Bassically, a middle man which can handle async well.
It's built in Go to have a small footprint on server resources. It uses 3MB RAM and it has a size of 8MB.
How to use go-sse-wsgi-sidecar
Here is how you can use it.
Add this in your .env file (both services need to have access to it use env_file in docker compose):
GO_SSE_SIDECAR_HOST=localhost
GO_SSE_SIDECAR_PORT=5687
GO_SSE_SIDECAR_REDIS_URL=redis://:your-password@localhost-or-docker-container-name:6379/0
GO_SSE_SIDECAR_TOKEN=secret-token-here
Add this to your docker compose file. Or, use the Dockerfile in this repo. You also have the option to download the binary available on releases.
services:
sse_sidecar:
image: climentea/go-sse-wsgi-sidecar:latest
container_name: sse-sidecar
restart: unless-stopped
ports:
- "5687:5687"
env_file:
- .env
On the Python/Django app:
- Install
pyjwtpackage - this will be used to make sure only the authentificated user can have access to the server sent events. - Install
redispackage - we'll use here redis pub/sub functionality to have the Django app sending events and this app to send those recived events to frontend. If you already have celery/django-rq or other similar packages you could use the same connection as I did.
Add this view to your main urls.py file. This view will be used by frontend to get an authorization token.
import jwt
import datetime
from django.conf import settings
from django.http import JsonResponse
@login_required
def get_sse_token_view(request):
exp_dt = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
minutes=15
)
payload = {
"user_id": request.social_user_id,
"exp": exp_dt,
}
try:
token = jwt.encode(payload, settings.GO_SSE_SIDECAR_TOKEN, algorithm="HS256")
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)
return JsonResponse(
{
"token": token,
"expires_in": 900,
}
)
urlpatterns = [
# etc
path("admin/", admin.site.urls),
path("sse-token/", get_sse_token_view, name="sse_token"),
]
I've used the connection of django_rq because it was already in my setup, but you can create a new redis connection if you want.
It must be the same connection for both services so they can write to the same pub/sub server.
Add this somewhere in your utils package:
import json
import django_rq
def publish(event_name: str, data: dict):
r = django_rq.get_connection()
r.publish("events", json.dumps({"event": event_name, "data": data}))
In your main scripts.js file or in base.html file add this EventSource listener.
You can change the urls based on what ports you've exposed.
When you'll run the app entirely in docker compose change localhost with the name of the service (Django service and Go service).
Add your handlers on alert(JSON.stringify(e.data)); and make it do react on a new event however you want.
let evtSource = null;
async function startSSE() {
const res = await fetch("http://localhost:8000/sse-token");
const { token } = await res.json();
evtSource = new EventSource(`http://localhost:5687/sse-events?ssetoken=${token}`);
evtSource.onmessage = (e) => {
console.log("Received:", e.data);
alert(JSON.stringify(e.data));
};
evtSource.onerror = (e) => {
console.error("SSE error", e);
evtSource.close();
setTimeout(startSSE, 2000);
};
}
startSSE();
Cool, now just import publish function where you need and start sending how many events you want to frontend.
You can see the code here.

Top comments (0)