Django WebSockets with Channels: consumers, rooms, and deployment

Build Django Channels echo and room endpoints, validate messages and permissions, configure ASGI and Redis, and prepare persistent connections for production.

Best practices
Illustration of logs flowing through a collection pipeline

Django Channels lets a Django application handle WebSocket connections alongside ordinary HTTP requests. Use it when the browser and server both need to send messages over a persistent connection—for chat, collaborative interfaces, notifications, or job progress.

WebSocket is one option for real-time features. Server-sent events (SSE) may be enough for server-to-browser updates, and HTTP streaming or polling can fit other workloads. Channels supplies ASGI routing, consumers, authentication integration, and optional communication between application instances.

Configure the project

Start with a Django project named myproject and an application named chat. Install the server and channel-layer packages:

python -m pip install channels daphne channels-redis

Add Daphne before django.contrib.staticfiles in your existing INSTALLED_APPS list; keep the rest of the project's apps. Add channels and chat if they are not already present. Then configure the ASGI entry point and development hosts:

# myproject/settings.py
ASGI_APPLICATION = "myproject.asgi.application"
ALLOWED_HOSTS = ["localhost", "127.0.0.1"]

# Single-process development and tests only.
CHANNEL_LAYERS = {
    "default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}
}

The echo example does not need a channel layer. Broadcasting later in this guide does. For communication across worker processes or hosts, replace the in-memory configuration with Redis:

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

That configuration expects a Redis server at the given address. In-memory layers are separate in every process, so they cannot deliver broadcasts between workers. channels_redis is the officially maintained production channel-layer backend. Configure its network access and security for your deployment.

Daphne serves ASGI traffic, including both HTTP and WebSocket connections. You can also keep a separate WSGI server for ordinary HTTP and route only WebSocket traffic to an ASGI server.

Write an echo consumer

A consumer handles WebSocket lifecycle events much as a Django view handles an HTTP request. This synchronous example accepts a connection, validates a small JSON message, and echoes its msg value:

# chat/consumers.py
import json

from channels.generic.websocket import WebsocketConsumer


def parse_message(text_data):
    if text_data is None or len(text_data) > 4096:
        raise ValueError("Expected a small text message")
    data = json.loads(text_data)
    if not isinstance(data, dict):
        raise ValueError("Expected a JSON object")
    msg = data.get("msg")
    if not isinstance(msg, str) or not 1 <= len(msg) <= 1000:
        raise ValueError("msg must contain 1 to 1000 characters")
    return msg


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

    def receive(self, text_data=None, bytes_data=None):
        try:
            msg = parse_message(text_data)
        except (ValueError, TypeError):
            self.close(code=1008)
            return
        self.send(text_data=json.dumps({"echo": msg}))

    def disconnect(self, close_code):
        pass

The length check limits this application's accepted content after receipt; it does not replace WebSocket server frame-size and rate limits. This echo endpoint is public to allowed origins. Add authorization before accepting if the data requires it.

Register the endpoint:

# chat/routing.py
from django.urls import path
from .consumers import EchoConsumer

websocket_urlpatterns = [path("ws/echo/", EchoConsumer.as_asgi())]

Initialize Django before importing routes that might import models:

# myproject/asgi.py
import os

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

from django.core.asgi import get_asgi_application

django_asgi_app = get_asgi_application()

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from chat.routing import websocket_urlpatterns

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

AuthMiddlewareStack makes the Django session user available in scope["user"]; it does not require that user to be logged in. AllowedHostsOriginValidator restricts browser origins using ALLOWED_HOSTS. Keep that list specific in production.

Run the project's migrations for the authentication/session tables, then start the development server:

python manage.py migrate
python manage.py runserver

From a page served by the same application, connect using the matching secure or insecure WebSocket scheme:

const scheme = window.location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(`${scheme}://${window.location.host}/ws/echo/`);
ws.onmessage = (event) => console.log(JSON.parse(event.data));
ws.onopen = () => ws.send(JSON.stringify({ msg: "Hello" }));
ws.onclose = (event) => console.log("WebSocket closed", event.code);

Broadcast to a room

Groups let one consumer send an event to all the connections subscribed to a room. The following deliberately small policy permits authenticated users to join one shared room, lobby. Replace that policy with a real room-membership check before supporting private rooms.

Append this consumer to chat/consumers.py so it can reuse json, WebsocketConsumer, and parse_message:

from asgiref.sync import async_to_sync


class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.group_name = None
        room = self.scope["url_route"]["kwargs"]["room"]
        user = self.scope["user"]
        if not user.is_authenticated or room != "lobby":
            self.close()
            return

        self.group_name = "chat.lobby"
        async_to_sync(self.channel_layer.group_add)(
            self.group_name, self.channel_name
        )
        self.accept()

    def receive(self, text_data=None, bytes_data=None):
        try:
            msg = parse_message(text_data)
        except (ValueError, TypeError):
            self.close(code=1008)
            return
        async_to_sync(self.channel_layer.group_send)(
            self.group_name, {"type": "chat.message", "msg": msg}
        )

    def chat_message(self, event):
        self.send(text_data=json.dumps({"msg": event["msg"]}))

    def disconnect(self, close_code):
        if self.group_name is not None:
            async_to_sync(self.channel_layer.group_discard)(
                self.group_name, self.channel_name
            )

Replace the route file with both endpoints:

# chat/routing.py
from django.urls import path
from .consumers import ChatConsumer, EchoConsumer

websocket_urlpatterns = [
    path("ws/echo/", EchoConsumer.as_asgi()),
    path("ws/chat/<slug:room>/", ChatConsumer.as_asgi()),
]

Log in through the application's existing Django session login, then open two browser connections to /ws/chat/lobby/. Send the same { "msg": "Hello" } shape. Both connections, including the sender, receive the broadcast. An anonymous user or a different room is rejected.

Channel-layer methods are asynchronous, so a synchronous consumer calls them through async_to_sync. The event type chat.message selects the consumer's chat_message method. For dynamic groups, enforce backend group-name character and length limits; do not use arbitrary user input as a group name.

A channel layer is transport, not durable chat history. It does not provide application-level replay, acknowledgments, or guaranteed delivery to disconnected clients. Persist messages separately when reconnecting clients must recover missed updates. Channels' lightweight background-task facilities likewise do not replace a durable task queue with retries.

Prepare for production

Use Daphne or another suitable ASGI server, such as Uvicorn, under a process supervisor. Your reverse proxy must support WebSocket upgrades, TLS, and appropriate idle timeouts. A shared channel layer connects workers; WebSocket itself does not require sticky routing for future connections, although application-local state can introduce that requirement.

Measure active connections, connection/reconnection rate, messages per second, processing time, and queue pressure. Test both idle connection capacity and busy rooms: a single message can fan out to many clients. Include malformed messages, slow clients, Redis outages, and graceful shutdown in the test plan. Apply connection and message rate limits, and decide how the client reconnects without duplicating work.

Record connection and message outcomes without logging session cookies or full private conversations. In Guance, collect those application logs with stable service and connection/request identifiers, then compare them with server and Redis metrics.

Frequently asked questions

When should we choose Channels over a separate WebSocket service?

Channels fits real-time features that share Django authentication and business logic. At larger scale, benchmark the actual connection count, fan-out, and message workload before deciding whether a dedicated gateway should own persistent connections while Django serves business APIs.

Should the consumer be synchronous or asynchronous?

A synchronous WebsocketConsumer runs its handlers through Channels' thread-based machinery and is a sensible starting point for synchronous libraries and ORM work. An async consumer fits async-compatible I/O. Do not call blocking libraries directly on its event loop; use supported async ORM methods or database_sync_to_async for synchronous database operations, with attention to connection lifecycle.

How should WebSocket authentication work?

Django session authentication is available through the middleware stack. Token-based authentication needs explicit validation and room authorization. The browser's native WebSocket API cannot set arbitrary request headers. Avoid long-lived credentials in query strings, where proxies may log them; use an appropriate secure session or short-lived ticket and validate the origin. Long-lived connections also need a policy for logout or permission changes after the handshake.

References and related reading

This guide uses the Channels tutorial, channel-layer documentation, consumer guidance, and WebSocket security guidance. Its examples illustrate the application flow; they are not a complete production chat service.

Continue with Django logging, or the Chinese-language guides to Django versus Ruby on Rails and containerizing Django.

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting