DEV Community

Alin Climente
Alin Climente

Posted on

How to implement SSE in Django with WSGI

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.

docker-stats-go-sse-wsgi-sidecar

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

On the Python/Django app:

  • Install pyjwt package - this will be used to make sure only the authentificated user can have access to the server sent events.
  • Install redis package - 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"),
]

Enter fullscreen mode Exit fullscreen mode

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}))

Enter fullscreen mode Exit fullscreen mode

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();

Enter fullscreen mode Exit fullscreen mode

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)