Real-Time with Django Channels
Reviewed & published by Brayan K
Django Channels is an official extension that adds WebSockets and other long-lived connections to Django over the ASGI layer, so the server can push data to the browser in real time instead of only answering one request at a time.
Part of the free Django course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll write a WebSocket consumer with its connect, receive, and disconnect handlers, understand the channel layer backed by Redis, and broadcast to groups so one message reaches every socket in a room.
What You'll Learn in This Lesson
- What Channels adds on top of plain Django
- Writing a consumer: connect, receive, disconnect
- Routing WebSockets in the ASGI application
- The channel layer and why it uses Redis
- Joining groups with group_add / group_discard
- Broadcasting to a group with group_send
1 Writing a WebSocket Consumer
A consumer is to a WebSocket what a view is to an HTTP request — except the connection stays open. You subclass AsyncWebsocketConsumer and implement three lifecycle handlers: connect when the socket opens, receive each time a message arrives, and disconnect when it closes. The consumer accepts the connection, reacts to messages, and cleans up.
# chat/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
await self.accept() # open the socket
async def receive(self, text_data):
data = json.loads(text_data)
# echo the message straight back to this socket
await self.send(text_data=json.dumps({"echo": data["message"]}))
async def disconnect(self, close_code):
pass # clean up here# A WebSocket Consumer is the async equivalent of a view, but it stays
# CONNECTED. It has connect/receive/disconnect handlers. This simulates a
# consumer accepting a connection and echoing messages back.
class ChatConsumer:
def __init__(self):
self.connected = False
self.outbox = []
def connect(self):
self.connected = True
self.outbox.append("ACCEPT connection")
def receive(self, text_data):
if not self.connected:
raise RuntimeError("receive before connect")
# echo the message back to this socket
self.outbox.append(f"echo: {text_data}")
def disconnect(self, code):
self.connected = False
self.outbox.append(f"CLOSE ({code})")
c = ChatConsumer()
c.connect()
c.receive("hello")
c.receive("world")
c.disconnect(1000)
for line in c.outbox:
print(line)
# EXPECTED:
# ACCEPT connection
# echo: hello
# echo: world
# CLOSE (1000)
# ✅ Expected output:
# ACCEPT connection
# echo: hello
# echo: world
# CLOSE (1000)2 The Channel Layer, Groups, and Redis
One open socket talking to itself isn't useful. The channel layer is a shared message bus that lets consumers — even across different worker processes — reach each other through named groups. A consumer joins a group with group_add and leaves with group_discard. Because workers are spread across processes, the layer needs a backend they all share, which in production is Redis.
# settings.py — point the channel layer at Redis
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {"hosts": [("127.0.0.1", 6379)]},
},
}
# chat/consumers.py — join a per-room group on connect
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room = self.scope["url_route"]["kwargs"]["room_name"]
self.group = f"chat_{self.room}"
await self.channel_layer.group_add(self.group, self.channel_name)
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.group, self.channel_name)# The CHANNEL LAYER (backed by Redis in production) lets consumers talk to
# each other through GROUPS. A consumer joins a group, and anything sent to
# that group is delivered to EVERY member. This simulates the pub/sub layer.
class ChannelLayer:
def __init__(self):
self.groups = {} # group_name -> set of channel names
self.mailboxes = {} # channel name -> list of messages
def group_add(self, group, channel):
self.groups.setdefault(group, set()).add(channel)
self.mailboxes.setdefault(channel, [])
def group_discard(self, group, channel):
self.groups.get(group, set()).discard(channel)
def group_send(self, group, message):
# broadcast to every channel currently in the group
for channel in self.groups.get(group, set()):
self.mailboxes[channel].append(message)
layer = ChannelLayer()
# Three sockets join the "room_lobby" group
layer.group_add("room_lobby", "alice")
layer.group_add("room_lobby", "bob")
layer.group_add("room_lobby", "carol")
# Bob leaves before the broadcast
layer.group_discard("room_lobby", "bob")
# Alice sends a message to the whole group
layer.group_send("room_lobby", "Alice: hi everyone!")
for channel in ["alice", "bob", "carol"]:
print(f"{channel} received: {layer.mailboxes[channel]}")
# EXPECTED:
# alice received: ['Alice: hi everyone!']
# bob received: []
# carol received: ['Alice: hi everyone!']
# ✅ Expected output:
# alice received: ['Alice: hi everyone!']
# bob received: []
# carol received: ['Alice: hi everyone!']3 Broadcasting and Routing
To send a chat message to everyone in a room, the consumer calls group_send with an event whose type names a handler method. The layer then invokes that method — chat_message here — on every consumer in the group, and each one sends the text down its own socket. Finally, you route WebSocket URLs in the ASGI application, the WebSocket equivalent of urls.py.
# chat/consumers.py
class ChatConsumer(AsyncWebsocketConsumer):
async def receive(self, text_data):
message = json.loads(text_data)["message"]
# broadcast to the whole group
await self.channel_layer.group_send(
self.group,
{"type": "chat_message", "message": message},
)
# named by the event's "type"; runs on every group member
async def chat_message(self, event):
await self.send(text_data=json.dumps({"message": event["message"]}))# 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()),
]
# project/asgi.py — wire HTTP and WebSocket protocols together
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
application = ProtocolTypeRouter({
"http": django_asgi_app,
"websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)),
})# Full picture: a Consumer joins a group on connect, broadcasts to the group
# on receive, and the channel layer delivers a group event to each member's
# handler. This ties the consumer and the channel layer together.
class ChannelLayer:
def __init__(self):
self.groups = {}
self.consumers = {} # channel -> consumer
def group_add(self, group, channel, consumer):
self.groups.setdefault(group, set()).add(channel)
self.consumers[channel] = consumer
def group_send(self, group, event):
for channel in self.groups.get(group, set()):
# the layer calls the named handler on each consumer
handler = getattr(self.consumers[channel], event["type"])
handler(event)
class ChatConsumer:
def __init__(self, name, layer):
self.name = name
self.layer = layer
self.received = []
def connect(self):
self.layer.group_add("lobby", self.name, self)
def receive(self, text):
# broadcast to the whole group via the channel layer
self.layer.group_send("lobby", {"type": "chat_message", "text": text, "from": self.name})
def chat_message(self, event):
# this handler is invoked for every group member
self.received.append(f"{event['from']}: {event['text']}")
layer = ChannelLayer()
alice = ChatConsumer("alice", layer)
bob = ChatConsumer("bob", layer)
alice.connect()
bob.connect()
alice.receive("Hello!")
bob.receive("Hi Alice!")
print("alice sees:", alice.received)
print("bob sees:", bob.received)
# EXPECTED:
# alice sees: ['alice: Hello!', 'bob: Hi Alice!']
# bob sees: ['alice: Hello!', 'bob: Hi Alice!']
# ✅ Expected output:
# alice sees: ['alice: Hello!', 'bob: Hi Alice!']
# bob sees: ['alice: Hello!', 'bob: Hi Alice!']🎯 Your Turn: Broadcast to the Group
Replace the blank with the group name "lobby" so both members receive the broadcast.
# 🎯 YOUR TURN: broadcast a message to everyone in a group.
# Replace ___ with the group name "lobby" so both members receive it.
class Layer:
def __init__(self):
self.groups = {"lobby": ["alice", "bob"]}
self.inbox = {"alice": [], "bob": []}
def group_send(self, group, message):
for channel in self.groups[group]:
self.inbox[channel].append(message)
layer = Layer()
layer.group_send(___, "Welcome!")
print("alice:", layer.inbox["alice"])
print("bob:", layer.inbox["bob"])
# EXPECTED OUTPUT:
# alice: ['Welcome!']
# bob: ['Welcome!']Common Errors (And How to Fix Them)
❌ The WebSocket connects then immediately closes
Your connect never called self.accept().
✅ Fix: call await self.accept() at the end of connect.
❌ Messages don't reach other users
No channel layer is configured, or consumers never joined a group.
✅ Fix: set CHANNEL_LAYERS to Redis and call group_add in connect.
❌ "No handler for message type ..."
The event type doesn't match a method on the consumer.
✅ Fix: define a method named after the type (e.g. chat_message for type "chat_message").
📋 Quick Reference
| Item | What It Does |
|---|---|
| AsyncWebsocketConsumer | Base class for a WebSocket consumer |
| self.accept() | Open the WebSocket connection |
| group_add / group_discard | Join or leave a named group |
| group_send(group, event) | Broadcast an event to every group member |
| CHANNEL_LAYERS | Setting pointing the layer at Redis |
🎯 Mini Challenge: A Multi-Room Pub/Sub System
Build a tiny channel layer with multiple rooms. A message published to the sports room must reach only its subscribers, never the music room.
# 🎯 Mini Challenge: build a tiny pub/sub system with multiple rooms.
# Consumers subscribe to a room; publish delivers only to that room's
# members. Show that a message in "sports" never reaches "music" members.
class ChannelLayer:
def __init__(self):
self.rooms = {}
self.inbox = {}
def subscribe(self, room, user):
self.rooms.setdefault(room, set()).add(user)
self.inbox.setdefault(user, [])
def publish(self, room, message):
for user in self.rooms.get(room, set()):
self.inbox[user].append(f"[{room}] {message}")
layer = ChannelLayer()
layer.subscribe("sports", "alice")
layer.subscribe("sports", "bob")
layer.subscribe("music", "carol")
layer.publish("sports", "Goal!")
layer.publish("music", "New album out")
for user in ["alice", "bob", "carol"]:
print(f"{user}: {layer.inbox[user]}")
# EXPECTED:
# alice: ['[sports] Goal!']
# bob: ['[sports] Goal!']
# carol: ['[music] New album out']❓ Frequently Asked Questions
Lesson complete — your app can talk in real time!
You wrote a WebSocket consumer with connect, receive, and disconnect, configured the Redis-backed channel layer, joined and left groups, and broadcast a message to every socket in a room with group_send.
Practice quiz
What does Django Channels add to Django?
- A faster ORM
- Support for WebSockets and long-lived connections
- Automatic migrations
- A new admin theme
Answer: Support for WebSockets and long-lived connections. Channels adds WebSockets and other long-lived connections over the ASGI layer.
What is a consumer in Channels?
- A database model
- A URL pattern
- The equivalent of a view for a persistent connection
- A cache backend
Answer: The equivalent of a view for a persistent connection. A consumer handles a connection's lifecycle: connect, receive, and disconnect.
Which handler runs each time a WebSocket message arrives?
- connect
- disconnect
- accept
- receive
Answer: receive. receive runs every time a message arrives on the open socket.
What must connect() call so the WebSocket handshake succeeds?
- self.accept()
- self.open()
- self.start()
- self.connect()
Answer: self.accept(). Without self.accept() (or self.close()), the browser's handshake fails.
What is the channel layer?
- A template engine
- A shared message bus that lets consumers reach each other
- A migration tool
- A static file server
Answer: A shared message bus that lets consumers reach each other. The channel layer is a shared bus letting consumers across processes communicate.
Which backend is standard for the channel layer in production?
- SQLite
- Memcached
- Redis (via channels_redis)
- PostgreSQL
Answer: Redis (via channels_redis). channels_redis backs the channel layer with Redis so all workers share groups.
Which methods join and leave a group?
- join / leave
- subscribe / unsubscribe
- open / close
- group_add / group_discard
Answer: group_add / group_discard. A consumer joins with group_add and leaves with group_discard.
How do you broadcast an event to every consumer in a group?
- channel_layer.group_send(group, event)
- channel_layer.broadcast(event)
- self.send_all(event)
- group.publish(event)
Answer: channel_layer.group_send(group, event). group_send delivers the event to a matching handler on every group member.
In a group event, what does the 'type' key determine?
- The Redis host
- The group name
- The handler method name invoked on each consumer
- The close code
Answer: The handler method name invoked on each consumer. The event's type names the handler method, e.g. type 'chat_message' calls chat_message.
What is a group in Channels?
- A Django permission set
- A named set of channels
- A database table
- An HTTP middleware
Answer: A named set of channels. A group is a named set of channels, like all sockets in one chat room.
Continue this course
- Previous: Async Views & ASGI
- Next: Background Tasks with Celery — Run slow work off-request with shared tasks, brokers, and retries
- Quick reference: Django cheat sheet
Frequently asked questions
What is Django Channels?
Django Channels is an official extension that adds support for long-lived connections such as WebSockets to Django, alongside the usual request and response cycle. Plain Django speaks HTTP, where the client asks and the server answers once. Channels lets the server keep a connection open and push data to the browser whenever something happens, which is what you need for chat, live notifications, dashboards, and multiplayer features. It builds on the ASGI interface rather than the older WSGI one.
What is a consumer in Channels?
A consumer is the Channels equivalent of a view, but for a persistent connection. Where a view handles one request and returns one response, a consumer has handler methods for the lifecycle of a connection: connect runs when the WebSocket opens, receive runs each time a message arrives, and disconnect runs when it closes. You typically subclass AsyncWebsocketConsumer and implement those methods to accept the socket, react to incoming messages, and clean up on close.
What is a channel layer and why does it need Redis?
The channel layer is a shared message bus that lets different consumer instances — even ones running in different worker processes — talk to each other. A consumer adds its channel to a named group, and anything sent to that group is delivered to every member. Because your consumers may be spread across many processes or servers, the layer needs an external backend they all share, and Redis is the standard choice in production via channels_redis. An in-memory layer exists for tests but does not work across processes.
What are groups and broadcasting in Channels?
A group is a named set of channels, like all the sockets in one chat room. A consumer joins with group_add and leaves with group_discard, usually in its connect and disconnect handlers. To broadcast, you call group_send on the channel layer with the group name and an event dictionary; the layer then invokes a matching handler method on every consumer in that group. This is the pub/sub pattern: one message in, many sockets out, which is exactly how a chat message reaches everyone in the room.