Skip to content
Codeloom
Django

Django Channels: Real-Time WebSockets

Add real-time WebSocket support to Django with Channels — consumers, routing, channel layers, groups, and building a live chat.

·6 min read · By Codeloom
Intermediate 14 min read

What you'll learn

  • How Django Channels extends Django beyond HTTP
  • How to write WebSocket consumers for real-time communication
  • How channel layers and groups enable broadcasting
  • How to build a live chat room step by step
  • How to deploy Channels with Daphne and Redis

Prerequisites

  • A working Django project
  • Basic understanding of WebSockets and async concepts
  • Redis installed locally

Django was built for HTTP: request in, response out. But modern applications need real-time features — live chat, notifications, dashboards that update without refreshing. Django Channels extends Django to handle WebSockets, long-polling, and other protocols by replacing the WSGI server with an ASGI server.

Installation and Setup

pip install channels channels-redis

Configure Channels in your Django project:

# settings.py
INSTALLED_APPS = [
    "daphne",  # Must be before django.contrib.staticfiles
    "channels",
    # ... your apps
    "django.contrib.staticfiles",
]

ASGI_APPLICATION = "myproject.asgi.application"

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("127.0.0.1", 6379)],
        },
    },
}

Create the ASGI application:

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

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

django_asgi_app = get_asgi_application()

from chat.routing import websocket_urlpatterns  # Import after django setup

application = ProtocolTypeRouter({
    "http": django_asgi_app,
    "websocket": AuthMiddlewareStack(
        URLRouter(websocket_urlpatterns)
    ),
})

ProtocolTypeRouter dispatches connections by protocol — HTTP goes to Django as usual, WebSocket connections go to your consumers.

Writing Your First Consumer

A consumer is the WebSocket equivalent of a view. It handles connect, receive, and disconnect events.

Synchronous Consumer

# chat/consumers.py
import json
from channels.generic.websocket import WebsocketConsumer


class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.accept()

    def disconnect(self, close_code):
        pass

    def receive(self, text_data):
        data = json.loads(text_data)
        message = data["message"]

        # Echo the message back
        self.send(text_data=json.dumps({
            "message": message,
        }))

Async consumers handle more concurrent connections without blocking:

# chat/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer


class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        await self.accept()

    async def disconnect(self, close_code):
        pass

    async def receive(self, text_data):
        data = json.loads(text_data)
        message = data["message"]

        await self.send(text_data=json.dumps({
            "message": message,
        }))

Routing

WebSocket URLs are defined separately from HTTP URLs:

# chat/routing.py
from django.urls import re_path
from . import consumers

websocket_urlpatterns = [
    re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi()),
]

The as_asgi() class method creates an ASGI application instance, similar to as_view() for HTTP views.

Channel Layers and Groups

The echo consumer above only talks to a single connection. For real-time features, you need to broadcast messages to multiple clients. Channel layers (backed by Redis) enable this.

Groups: Broadcasting to Multiple Clients

Groups let you add multiple WebSocket connections to a named channel and broadcast to all of them:

# chat/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer


class ChatRoomConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
        self.room_group_name = f"chat_{self.room_name}"

        # Join the room group
        await self.channel_layer.group_add(
            self.room_group_name,
            self.channel_name,
        )

        await self.accept()

    async def disconnect(self, close_code):
        # Leave the room group
        await self.channel_layer.group_discard(
            self.room_group_name,
            self.channel_name,
        )

    async def receive(self, text_data):
        data = json.loads(text_data)
        message = data["message"]
        username = data.get("username", "Anonymous")

        # Broadcast to the room group
        await self.channel_layer.group_send(
            self.room_group_name,
            {
                "type": "chat.message",  # Maps to chat_message method
                "message": message,
                "username": username,
            },
        )

    async def chat_message(self, event):
        """Handle messages broadcast to the group."""
        await self.send(text_data=json.dumps({
            "message": event["message"],
            "username": event["username"],
        }))

Key concepts:

  • self.channel_name — unique identifier for this WebSocket connection
  • group_add — adds a channel to a named group
  • group_send — sends a message to every channel in the group
  • type: "chat.message" — determines which method handles the event (dots become underscores: chat_message)

Building a Live Chat Room

The Template

<!-- chat/templates/chat/room.html -->
<!DOCTYPE html>
<html>
<body>
  <h1>Chat Room: {{ room_name }}</h1>
  <div id="chat-log" style="height: 400px; overflow-y: scroll; border: 1px solid #ccc; padding: 10px;"></div>
  <input id="chat-input" type="text" placeholder="Type a message..." />
  <button id="chat-send">Send</button>

  <script>
    const roomName = "{{ room_name }}";
    const chatSocket = new WebSocket(
      `ws://${window.location.host}/ws/chat/${roomName}/`
    );

    chatSocket.onmessage = function (e) {
      const data = JSON.parse(e.data);
      const chatLog = document.getElementById("chat-log");
      chatLog.innerHTML += `<p><strong>${data.username}:</strong> ${data.message}</p>`;
      chatLog.scrollTop = chatLog.scrollHeight;
    };

    chatSocket.onclose = function () {
      console.log("WebSocket closed unexpectedly");
    };

    document.getElementById("chat-send").onclick = function () {
      const input = document.getElementById("chat-input");
      chatSocket.send(JSON.stringify({
        message: input.value,
        username: "{{ request.user.username|default:'Guest' }}",
      }));
      input.value = "";
    };

    document.getElementById("chat-input").onkeyup = function (e) {
      if (e.key === "Enter") {
        document.getElementById("chat-send").click();
      }
    };
  </script>
</body>
</html>

The View

# chat/views.py
from django.shortcuts import render


def chat_room(request, room_name):
    return render(request, "chat/room.html", {"room_name": room_name})
# chat/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("<str:room_name>/", views.chat_room, name="chat-room"),
]

Persisting Messages to the Database

Use database_sync_to_async to call Django ORM methods from async consumers:

from channels.db import database_sync_to_async
from .models import Message, Room


class ChatRoomConsumer(AsyncWebsocketConsumer):
    async def receive(self, text_data):
        data = json.loads(text_data)
        message = data["message"]
        username = data.get("username", "Anonymous")

        # Save to database
        await self.save_message(username, message)

        # Broadcast to group
        await self.channel_layer.group_send(
            self.room_group_name,
            {
                "type": "chat.message",
                "message": message,
                "username": username,
            },
        )

    @database_sync_to_async
    def save_message(self, username, message):
        room = Room.objects.get(name=self.room_name)
        Message.objects.create(room=room, username=username, content=message)

Authentication in WebSockets

AuthMiddlewareStack in asgi.py populates self.scope["user"] from the session cookie, just like request.user in HTTP views:

async def connect(self):
    self.user = self.scope["user"]

    if self.user.is_anonymous:
        await self.close()
        return

    await self.accept()

For token-based auth (JWT), write custom middleware:

# chat/middleware.py
from channels.middleware import BaseMiddleware
from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser
from rest_framework_simplejwt.tokens import AccessToken
from django.contrib.auth import get_user_model

User = get_user_model()


class JWTAuthMiddleware(BaseMiddleware):
    async def __call__(self, scope, receive, send):
        query_string = scope.get("query_string", b"").decode()
        token = dict(
            param.split("=") for param in query_string.split("&") if "=" in param
        ).get("token")

        scope["user"] = await self.get_user(token) if token else AnonymousUser()
        return await super().__call__(scope, receive, send)

    @database_sync_to_async
    def get_user(self, token_str):
        try:
            token = AccessToken(token_str)
            return User.objects.get(id=token["user_id"])
        except Exception:
            return AnonymousUser()

Sending Messages from Outside Consumers

Trigger WebSocket messages from views, Celery tasks, or management commands:

# In a view or task
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync

channel_layer = get_channel_layer()

async_to_sync(channel_layer.group_send)(
    "chat_general",
    {
        "type": "chat.message",
        "message": "System: server maintenance in 5 minutes",
        "username": "System",
    },
)

This is useful for notifications triggered by background tasks, admin actions, or webhooks.

Deployment

In production, replace Django’s development server with Daphne (ASGI server):

pip install daphne
daphne -b 0.0.0.0 -p 8000 myproject.asgi:application

Or use uvicorn:

pip install uvicorn
uvicorn myproject.asgi:application --host 0.0.0.0 --port 8000

Put an ASGI-aware reverse proxy (Nginx) in front:

upstream channels_backend {
    server localhost:8000;
}

server {
    location /ws/ {
        proxy_pass http://channels_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }

    location / {
        proxy_pass http://channels_backend;
        proxy_set_header Host $host;
    }
}

The Upgrade and Connection headers are required for WebSocket connections to pass through Nginx.

Summary

Django Channels brings real-time capabilities to Django through ASGI and WebSocket consumers. Use channel layers with Redis to broadcast messages across groups. Handle authentication with AuthMiddlewareStack or custom JWT middleware. Deploy with Daphne or uvicorn behind Nginx. For most real-time features — chat, notifications, live dashboards — Channels integrates naturally with Django’s auth, ORM, and middleware ecosystem.