Skip to main content

Command Palette

Search for a command to run...

Building SSE in Django with Django Channels

Updated
5 min read
Building SSE in Django with Django Channels

Intro to SSE

Server-Sent Events (SSE) let servers push updates to the browser over plain HTTP. An SSE client opens a single long-lived HTTP connection and the server streams text events down that connection as they become available.

Difference from normal request–response:

  • Normal: client requests → server responds once.

  • SSE: client opens connection → server keeps it open → server sends events as needed.

Think of SSE as a one-way WebSocket (server → client only). It’s a great fit for notifications, progress updates, live feeds, and other server-to-client streams where you don’t need client → server real-time channels.

When to prefer SSE over WebSockets:

  • You only need server → client updates.

  • You prefer a simpler HTTP-based approach (no websocket handshake).

  • You want built-in browser features like automatic reconnection, event naming, and last-event-id support.


Stack

  • Django — main framework

  • Django Channels — async primitives and channel layer/groups

  • Daphne — ASGI server

  • Redis — channel layer backend (message broker)


Note: This demo is currently being used in internal and UAT environment, the actual production environment behaviour and use-cases are yet to be seen.

Install

pip install channels daphne channels-redis

Basic settings

Add Daphne and Channels to INSTALLED_APPS and configure the channel layer (example using Redis):

# settings.py
INSTALLED_APPS = [
    # ...
    "daphne",
    "channels",
    # ...
]

ASGI_APPLICATION = "myproject.asgi.application"

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("127.0.0.1", 6379)],
            "capacity": 2000,
            "expiry": 60,
            "group_expiry": 3600,
        },
    },
}

ASGI routing

Route the SSE path to an AsyncHttpConsumer. Put specific Channels routes before the fallback get_asgi_application() so normal Django views still work.

# myproject/asgi.py
import os
import django
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from django.urls import path

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
django.setup()

from myapp.consumers import SSEConsumer  # your SSE consumer

application = ProtocolTypeRouter(
    {
        "http": URLRouter(
            [
                path("sse/", SSEConsumer.as_asgi()),  # SSE route first
                path("", get_asgi_application()),     # fallback to Django
            ]
        ),
        # "websocket": ... if you also use websockets
    }
)

Order matters: route the SSE URL first so Channels picks the consumer for that path.


Base SSE Consumer

This consumer:

  • Joins a channel-layer group so other parts of the app can broadcast to connected clients.

  • Sets SSE headers and streams events.

  • Sends periodic heartbeats to keep proxies from closing the connection.

  • Handles events delivered via the channel layer.

  • For More Check Here: Channels Docs

# myapp/consumers.py
import asyncio
import json
from channels.generic.http import AsyncHttpConsumer

class BaseSSEConsumer(AsyncHttpConsumer):
    group_name = None
    keep_running = True

    async def handle(self, body):
        self.group_name = await self.get_group_name()
        await self.channel_layer.group_add(self.group_name, self.channel_name)

        await self.send_headers(
            headers=[
                (b"Cache-Control", b"no-cache"),
                (b"Content-Type", b"text/event-stream"),
                (b"Transfer-Encoding", b"chunked"),
                (b"Connection", b"keep-alive"),
            ]
        )

        self.heartbeat_task = asyncio.create_task(self._heartbeat())

    async def _heartbeat(self):
        while self.keep_running:
            try:
                await self.send_body(b"event: ping\ndata: pong\n\n", more_body=True)
            except Exception:
                break
            await asyncio.sleep(30)

    async def sse_event(self, event: dict):
        event_key = event.get("event")
        data_payload = event.get("data")
        data = f"event: {event_key}\ndata: {json.dumps(data_payload)}\n\n"
        await self.send_body(data.encode("utf-8"), more_body=True)

    async def http_request(self, message):
        await self.handle(b"")

    async def http_disconnect(self, message):
        self.keep_running = False
        if hasattr(self, "heartbeat_task"):
            self.heartbeat_task.cancel()
        if self.group_name:
            await self.channel_layer.group_discard(self.group_name, self.channel_name)

(Adapt get_group_name, authentication, and event dispatching to your app's needs.)


Broadcasting events from Django Use Channels’ channel layer to send events to the SSE group.

Synchronous view/function:

# myapp/utils.py
# any module where you want to broadcast
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer

def broadcast_notification(data):
    channel_layer = get_channel_layer()
    async_to_sync(channel_layer.group_send)(
        "notifications",
        {
            "type": "sse.event",
            "event": "notification",
            "data": data,
        },
    )

# Using it in a view.
# myapp/views.py
def mark_done(request):
    # ... do work
    broadcast_notification({"msg": "Task completed", "id": 123})
    return JsonResponse({"ok": True})

Nginx configuration for SSE

For SSE to work reliably behind Nginx you must disable buffering and ensure long timeouts. Add an upstream block for Daphne (TCP or unix socket) and a location for the SSE endpoint.

Example upstream (TCP):

upstream sse_upstream {
    server 127.0.0.1:9095;
}

Insert the provided SSE location block into your server block so Nginx streams responses immediately:

location /sse/ {
    proxy_pass http://sse_upstream$request_uri;

    proxy_http_version 1.1;

    proxy_set_header Host $host;
    proxy_set_header Connection '';
    proxy_set_header X-Accel-Buffering no;

    proxy_buffering off;
    proxy_cache off;

    chunked_transfer_encoding on;
    gzip off;

    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;

    add_header Cache-Control no-cache;
}

Notes:

  • proxy_buffering off and X-Accel-Buffering no help Nginx stream events immediately to clients.

  • proxy_http_version 1.1 and clearing Connection header let chunked responses flow correctly.

  • Long proxy_read_timeout/proxy_send_timeout keep long-lived SSE connections from being closed prematurely.

  • chunked_transfer_encoding on and gzip off avoid interfering with streaming behavior.


Running Daphne

You can run Daphne directly for testing or under a process manager in production. Here is the exact command you provided (keeps HTTP timeout disabled and exposes Daphne on 0.0.0.0:9095):

uv run daphne \
  -b 0.0.0.0 \
  -p 9095 \
  --http-timeout 0 \
  --proxy-headers \
  --access-log - \
  "myproject.asgi:application"

Thank you.