Introduction
SSE is a long-lived HTTP response suitable for delivering message in realtime to the user. The browser makes a normal GET request, and instead of returning a response and closing the connection, the server keeps it open and streams events as they occur.
event: update
data: Hello, world!
event: update
data: Your report has finished generating.
event: update
data: 75%
Each event is separated by a blank line. You can consume these events in the browser via the EventSource API:
const es = new EventSource("/events/");
es.addEventListener("update", (e) => {
console.log(e.data);
});
You can use SSE through ASGI and Django's StreamingHttpReponse.
Simple Example
Use your preferred method to spin up a new Django project. I'll use uv:
uv init demo
cd demo
uv add django gunicorn
uv run django-admin startproject demo .
The only two dependencies we need are Django and any ASGI-powered application server.
For an example this small, we'll place everything in demo/urls.py:
import asyncio
from datetime import datetime
from django.http import HttpResponse, StreamingHttpResponse
from django.urls import path
def index(request):
return HttpResponse("""\
<!doctype html>
<title>SSE demo</title>
<h1>Current time: <span id="message">...</span></h1>
<script>
const es = new EventSource("/events/");
es.addEventListener("update", (e) => {
document.getElementById("message").textContent = e.data;
});
</script>
""")
async def events(request):
async def stream():
yield "retry: 3000\n\n"
while True:
now = datetime.now().strftime("%H:%M:%S")
yield f"event: update\ndata: {now}\n\n"
await asyncio.sleep(1)
response = StreamingHttpResponse(
stream(),
content_type="text/event-stream",
)
response["Cache-Control"] = "no-cache"
return response
urlpatterns = [
path("", index),
path("events/", events),
]
Now all we need to do is start the application:
uv run gunicorn --worker-class asgi demo.asgi:application
Open http://127.0.0.1:8000 and you'll see the clock update every second. Here's a quick explanation on important parts of that code block:
text/event-streamtells the browser this is an SSE stream.\n\nterminates an event. Forget it and the browser waits forever.retry: 3000tells the browser to reconnect after three seconds if the connection is lost.event:(optional) names the event foraddEventListener().data:contains the event payload.
More Realistic Example
Our previous example generated an event every second using asyncio.sleep() which is only helpful for demonstrating SSE, but real applications send updates when something happens, usually from an external source (to the active process).
A common way to do this is to use Redis Pub/Sub. Instead of waiting for a timer, our SSE view waits for messages published to a Redis channel.
- The
eventsview subscribes to a Redis channel. - Another part of the application performs some work.
- Whenever progress changes, it publishes a message to Redis.
- The SSE view receives that message and immediately forwards it to every connected browser.
Install the Redis client then start Redis locally (I'll use docker):
uv add redis
docker run -d -p 6379:6379 redis:7-alpine
Now change the SSE view to:
import redis.asyncio as redis
redis_client = redis.from_url("redis://localhost:6379")
async def events(request):
async def stream():
pubsub = redis_client.pubsub()
await pubsub.subscribe("updates")
try:
yield "retry: 3000\n\n"
while True:
message = await pubsub.get_message(
ignore_subscribe_messages=True,
timeout=None,
)
if message is None:
continue
yield (
"event: update\n"
f"data: {message['data'].decode()}\n\n"
)
finally:
await pubsub.aclose()
response = StreamingHttpResponse(
stream(),
content_type="text/event-stream",
)
response["Cache-Control"] = "no-cache"
return response
You can publish by pushing messages to the respective channel:
import redis
client = redis.Redis()
client.publish("updates", "25%")
client.publish("updates", "50%")
client.publish("updates", "75%")
client.publish("updates", "Done!")
Or directly from your terminal (assuming redis-cli is installed):
redis-cli PUBLISH updates "25%"
redis-cli PUBLISH updates "50%"
redis-cli PUBLISH updates "75%"
redis-cli PUBLISH updates "Done!"
Each published message is immediately received by the SSE view and streamed to every connected browser without refreshing the page.
Publisher
↓
Redis
↑
/events/ (Subscriber)
↓ stream
Browser
Notice how neither side knows about the other. The publisher has no idea browsers are connected, and the SSE view has no idea who published the message. Redis acts as the bridge between synchronous code that produces events and the asynchronous view that streams them.
You should also know that there's a limit of 6 open SSE connections per browser per domain, when not on HTTP/2.