Build a Real-Time Chat Room

Build a multi-room chat app with Django Channels and WebSockets.

Introduction

30 Second Summary

Think about how group chats work. You send a message and everyone in the conversation sees it instantly without refreshing the page.

In this project, you will build a real-time chat room server using Django Channels and WebSockets. Users can create rooms, join them, and exchange messages that appear instantly across all connected browsers.

What You'll Build

A multi-room chat application powered by Django Channels, where messages broadcast in real time via WebSockets and a Redis-backed channel layer.

By the end of this project, you'll have:

  • A Django project configured for ASGI with Daphne as the development server.
  • An async WebSocket consumer that handles connect, disconnect, and message events.
  • A Redis-backed channel layer enabling real-time group messaging across multiple browser tabs.
  • Secret Mission: Add usernames and message persistence so new users see chat history when they join a room.

Are there any prerequisites?

This project assumes you are familiar with Django basics (models, views, URLs, and templates). You also need Python and Docker installed on your machine.

Before We Start

Before We Start...

Set Up the Project

Real-time features need a different server architecture than standard Django. A regular Django view handles one HTTP request at a time, but a chat room needs to hold many connections open simultaneously and push messages to all of them at once.

In this step, you will install the packages that give Django real-time superpowers and configure a Redis server to relay messages between connected clients.

In this step, get ready to:
  • Create a virtual environment and install Django with Channels packages.
  • Start a Redis container with Docker.
  • Create the Django project and chat app, then configure settings for ASGI.
Create a virtual environment and install packages

A virtual environment keeps this project's dependencies isolated from your system Python. You will install Django along with three packages that add WebSocket support.

  • Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir django-chat
cd django-chat
  • Create a virtual environment by running:
python -m venv venv

macOS users seeing an error?

If python is not recognized on macOS, use python3 instead. Replace python with python3 in all commands throughout this project.

  • Activate the virtual environment by running:

macOS

source venv/bin/activate

You should see (venv) appear at the beginning of your terminal prompt.

Windows

venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt.

  • Install Django and the Channels packages by running:
pip install django==5.2.15 channels==4.3.2 daphne==4.2.2 channels-redis==4.3.0

What are these packages?

  • django==5.2.15 is the web framework you already know. Version 5.2 is the current Long-Term Support release.
  • channels==4.3.2 extends Django to handle WebSockets and other async protocols.
  • daphne==4.2.2 is an ASGI server that replaces Django's default development server so it can serve WebSocket connections.
  • channels-redis==4.3.0 provides a Redis-backed channel layer so messages can be broadcast across multiple consumer instances.
  • Verify the packages installed correctly by running:
python -c "import django, channels, daphne; print(django.__version__, channels.__version__, daphne.__version__)"
  • You should see 5.2.15 4.3.2 4.2.2 printed to the terminal.

Seeing an import error?

  • Make sure you activated the virtual environment first. You should see (venv) in your prompt.
  • If pip installed packages outside the venv, deactivate, delete the venv folder, and recreate it.

Help me troubleshoot my Django Channels package installation.

Start a Redis container with Docker

Django Channels uses a channel layer to relay messages between WebSocket consumers. Redis is the recommended backing store for this layer because it handles message routing with very low latency.

  • Make sure Docker Desktop is running. Press Cmd+Space (macOS) or the Windows key (Windows), type Docker Desktop, and press Enter to open it.
  • Start a Redis container by running this command in your terminal:
docker run --name redis-chat -d -p 6379:6379 redis:7

What does this command do?

  • --name redis-chat gives the container a friendly name you can reference later.
  • -d runs the container in detached mode (in the background).
  • -p 6379:6379 maps port 6379 on your machine to port 6379 inside the container so Django can reach Redis.
  • redis:7 specifies the official Redis 7 image from Docker Hub.
  • Verify the container is running by running:
docker ps
  • You should see a row with redis:7 in the IMAGE column, a STATUS of Up followed by a duration, 0.0.0.0:6379->6379/tcp in the PORTS column, and redis-chat in the NAMES column.

Container not showing up?

  • If Docker says the name redis-chat is already in use, remove the old container first with docker rm -f redis-chat and re-run the command above.
  • Make sure Docker Desktop is running. The docker CLI cannot connect to the daemon if Docker Desktop is not started.

Help me get my Redis Docker container running.

Create the Django project and chat app

Now you will scaffold the Django project and create the chat app. Then you will configure Django to use Daphne as its ASGI server and connect to the Redis channel layer.

  • Create the Django project by running:
django-admin startproject chatproject .

Why the dot at the end?

The . tells Django to create the project files in the current directory instead of creating an extra nested folder. This keeps your folder structure flat and simple.

  • Create the chat app by running:
python manage.py startapp chat
  • Confirm the files exist by running:
ls
  • You should see chat, chatproject, manage.py, and venv in the output.

Now you need to open the project in your code editor to modify the settings file.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code (or Cursor if you use Cursor) and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to Desktop > django-chat, select it, and click Open.
  • In the Explorer sidebar, expand the chatproject folder and click on settings.py to open it.

You need to make three changes to this file. First, update the INSTALLED_APPS list.

  • Find the INSTALLED_APPS list in settings.py and replace it with the following:
INSTALLED_APPS = [
    'daphne',
    'chat',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

Why is daphne first?

Daphne must be at the top of INSTALLED_APPS so it takes over the runserver management command. This replaces Django's default WSGI development server with an ASGI server capable of handling WebSocket connections.

  • Scroll to the bottom of settings.py and add these lines below the existing settings:
# Point Django to the ASGI application
ASGI_APPLICATION = "chatproject.asgi.application"

# Configure the channel layer to use Redis
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("127.0.0.1", 6379)],
        },
    },
}

What do these settings do?

  • ASGI_APPLICATION tells Django where your ASGI routing configuration lives. This is the entry point for all WebSocket and HTTP traffic.
  • CHANNEL_LAYERS configures a Redis-backed message bus. When one consumer sends a message to a group, Redis relays it to every other consumer in that group.
  • Save settings.py.
  • Switch back to your terminal and verify the ASGI server starts by running:
python manage.py runserver
  • Look at the terminal output. You should see the text Starting ASGI/Daphne in the startup messages. This confirms Daphne has taken over the development server.
  • Press Ctrl+C to stop the server.

Still seeing the default Django server?

  • Double-check that 'daphne' is the very first entry in INSTALLED_APPS. If another app overrides the runserver command and appears before Daphne, it will take priority.
  • Make sure you saved the file after editing.

Help me fix my Django ASGI/Daphne configuration.

✔️ Awesome, I've got everything!

Great. Double check you have saved settings.py and that runserver shows Starting ASGI/Daphne in the output.

ⓧ I'd like to double check the full code

Here is the complete settings file additions you should have at this point:

INSTALLED_APPS = [
    'daphne',
    'chat',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

ASGI_APPLICATION = "chatproject.asgi.application"

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

Your project foundation is in place. Next up, you will build the chat frontend so users can enter a room name and start chatting.

Build the Chat Frontend

Your Django project is configured for ASGI and ready to serve real-time connections. Now you need pages for users to interact with.

Every chat app needs at least two pages: a lobby where users pick a room, and the room itself where messages flow. In this step, you will build both pages and wire them up with Django views and URL patterns.

In this step, get ready to:
  • Create the lobby page where users enter a room name.
  • Create the room page with a WebSocket connection for real-time messaging.
Create the lobby page

Django templates live inside a specific directory structure: app_name/templates/app_name/. This convention lets Django find the right template even when multiple apps exist in one project.

  • Create the template directory structure by running this command:

macOS

mkdir -p chat/templates/chat

Windows

mkdir chat\templates\chat
  • Confirm the directory was created by running:
ls chat/templates/chat

The output should be empty (no files yet), but the command should run without an error.

  • Create a new file at chat/templates/chat/index.html and paste the following code:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Chat Rooms</title>
</head>
<body>
    <h1>Chat Rooms</h1>
    <p>Enter a room name to join:</p>
    <input id="room-name-input" type="text" size="50"/>
    <button id="room-name-submit">Join</button>

    <script>
        document.querySelector('#room-name-input').focus();
        document.querySelector('#room-name-input').onkeyup = function(e) {
            if (e.key === 'Enter') {
                document.querySelector('#room-name-submit').click();
            }
        };
        document.querySelector('#room-name-submit').onclick = function(e) {
            var roomName = document.querySelector('#room-name-input').value;
            window.location.pathname = '/chat/' + roomName + '/';
        };
    </script>
</body>
</html>

What does this template do?

  • The input field and button let the user type a room name and click Join.
  • The onkeyup listener triggers the button click when the user presses Enter, so they don't need to reach for the mouse.
  • The onclick handler redirects the browser to /chat/ROOM_NAME/, which you will wire up to a room view next.
  • Save index.html.

You won't see this page in your browser yet. Django needs a view and URL pattern to serve it.

✔️ Awesome, I've got everything!

Great. Make sure you saved the file before continuing.

ⓧ I'd like to double check the full code

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Chat Rooms</title>
</head>
<body>
    <h1>Chat Rooms</h1>
    <p>Enter a room name to join:</p>
    <input id="room-name-input" type="text" size="50"/>
    <button id="room-name-submit">Join</button>

    <script>
        document.querySelector('#room-name-input').focus();
        document.querySelector('#room-name-input').onkeyup = function(e) {
            if (e.key === 'Enter') {
                document.querySelector('#room-name-submit').click();
            }
        };
        document.querySelector('#room-name-submit').onclick = function(e) {
            var roomName = document.querySelector('#room-name-input').value;
            window.location.pathname = '/chat/' + roomName + '/';
        };
    </script>
</body>
</html>

Now you need three Python files to wire the template to a URL that Django can serve.

  • Open chat/views.py and replace its contents with the following code:
from django.shortcuts import render


def index(request):
    # Render the lobby page where users enter a room name
    return render(request, "chat/index.html")
  • Create a new file at chat/urls.py and paste the following code:
from django.urls import path

from . import views

urlpatterns = [
    # Lobby page at /chat/
    path("", views.index, name="index"),
]
  • Open chatproject/urls.py and replace its contents with the following code:
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("chat/", include("chat.urls")),
    path("admin/", admin.site.urls),
]

How does this wiring work?

Django resolves URLs from the outside in. A request to /chat/ hits chatproject/urls.py first, which strips the chat/ prefix and passes the remainder to chat/urls.py.

The empty string pattern in chat/urls.py matches what's left (nothing), so Django calls the index view, which renders your template.

  • Save all three files: chat/views.py, chat/urls.py, and chatproject/urls.py.
  • Start the development server by running:
python manage.py runserver
  • Open your browser and navigate to http://127.0.0.1:8000/chat/.

You should see a heading that says Chat Rooms, a text input, and a Join button.

Seeing a TemplateDoesNotExist error?

  • Check that your file lives at exactly chat/templates/chat/index.html. A missing level in the directory structure causes this error.
  • Make sure 'chat' is listed in INSTALLED_APPS in your settings.py. Django only searches templates inside installed apps.
  • Help me fix a TemplateDoesNotExist error for chat/index.html
Create the room page template

The room page is where the real-time magic happens. It opens a WebSocket connection to the server so messages can flow both ways without refreshing the page.

  • Create a new file at chat/templates/chat/room.html and paste the following code:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Chat Room: {{ room_name }}</title>
</head>
<body>
    <h1>Room: {{ room_name }}</h1>
    <textarea id="chat-log" cols="100" rows="20" readonly></textarea><br/>
    <input id="chat-message-input" type="text" size="80"/>
    <button id="chat-message-submit">Send</button>

    <script>
        var roomName = "{{ room_name }}";
        var chatSocket = new WebSocket(
            'ws://' + window.location.host + '/ws/chat/' + roomName + '/'
        );

        chatSocket.onmessage = function(e) {
            var data = JSON.parse(e.data);
            document.querySelector('#chat-log').value += (data.message + '\n');
        };

        chatSocket.onclose = function(e) {
            console.error('Chat socket closed unexpectedly');
        };

        document.querySelector('#chat-message-input').focus();
        document.querySelector('#chat-message-input').onkeyup = function(e) {
            if (e.key === 'Enter') {
                document.querySelector('#chat-message-submit').click();
            }
        };

        document.querySelector('#chat-message-submit').onclick = function(e) {
            var messageInputDom = document.querySelector('#chat-message-input');
            var message = messageInputDom.value;
            chatSocket.send(JSON.stringify({
                'message': message
            }));
            messageInputDom.value = '';
        };
    </script>
</body>
</html>

What does this template do?

  • The textarea displays incoming messages as a scrollable log.
  • new WebSocket(...) opens a persistent connection to ws://HOST/ws/chat/ROOM_NAME/. This is the URL your consumer will handle in the next step.
  • onmessage fires every time the server pushes a message down the socket. It parses the JSON and appends the text to the chat log.
  • onclick grabs the input value, sends it as JSON over the socket, and clears the input field.
  • Save room.html.

✔️ Awesome, I've got everything!

Great. Make sure you saved the file before continuing.

ⓧ I'd like to double check the full code

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Chat Room: {{ room_name }}</title>
</head>
<body>
    <h1>Room: {{ room_name }}</h1>
    <textarea id="chat-log" cols="100" rows="20" readonly></textarea><br/>
    <input id="chat-message-input" type="text" size="80"/>
    <button id="chat-message-submit">Send</button>

    <script>
        var roomName = "{{ room_name }}";
        var chatSocket = new WebSocket(
            'ws://' + window.location.host + '/ws/chat/' + roomName + '/'
        );

        chatSocket.onmessage = function(e) {
            var data = JSON.parse(e.data);
            document.querySelector('#chat-log').value += (data.message + '\n');
        };

        chatSocket.onclose = function(e) {
            console.error('Chat socket closed unexpectedly');
        };

        document.querySelector('#chat-message-input').focus();
        document.querySelector('#chat-message-input').onkeyup = function(e) {
            if (e.key === 'Enter') {
                document.querySelector('#chat-message-submit').click();
            }
        };

        document.querySelector('#chat-message-submit').onclick = function(e) {
            var messageInputDom = document.querySelector('#chat-message-input');
            var message = messageInputDom.value;
            chatSocket.send(JSON.stringify({
                'message': message
            }));
            messageInputDom.value = '';
        };
    </script>
</body>
</html>

Now you need a view to serve this template and a URL to route to it.

  • Open chat/views.py and find the line that says:
    return render(request, "chat/index.html")
  • Add the following room view below the index function, so the full file looks like this:
from django.shortcuts import render


def index(request):
    # Render the lobby page where users enter a room name
    return render(request, "chat/index.html")


def room(request, room_name):
    # Render the chat room page, passing the room name to the template
    return render(request, "chat/room.html", {"room_name": room_name})
  • Open chat/urls.py and find the line:
    path("", views.index, name="index"),
  • Add a new path below it for the room view, so the full file looks like this:
from django.urls import path

from . import views

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

What does the room URL pattern do?

The <str:room_name> part is a path converter. It captures whatever text the user typed (like lobby or project-chat) and passes it to the room view as the room_name argument.

The view then injects that value into the template context so the JavaScript can build the correct WebSocket URL.

  • Save both chat/views.py and chat/urls.py.
  • Return to your browser at http://127.0.0.1:8000/chat/.
  • Type lobby into the input field and click Join.

You should land on a page showing Room: lobby with a textarea and a message input.

Seeing a WebSocket error in the console? That is expected

If you open your browser's developer console, you will see a connection error for the WebSocket. This is normal because you haven't built the server-side consumer yet. You will fix this in the next step.

✔️ Awesome, I've got everything!

Great. Double check you have saved all files before moving on.

ⓧ I'd like to double check the full code

from django.shortcuts import render


def index(request):
    return render(request, "chat/index.html")


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

from . import views

urlpatterns = [
    path("", views.index, name="index"),
    path("<str:room_name>/", views.room, name="room"),
]
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("chat/", include("chat.urls")),
    path("admin/", admin.site.urls),
]

Page not found or wrong page showing?

  • Make sure chatproject/urls.py uses include("chat.urls") and not a direct view reference.
  • Check that your chat/urls.py file has the trailing slash in <str:room_name>/. Django is strict about trailing slashes by default.
  • Help me debug why my Django chat room URL is not resolving

Your frontend is ready and waiting for a WebSocket connection. Next up, you will write the server-side consumer that accepts those connections and broadcasts messages to everyone in the room.

Write the WebSocket Consumer

Your chat frontend is ready and waiting. Right now, the room page tries to open a WebSocket connection, but nothing on the server knows how to handle it.

In this step, you will write the server-side code that accepts WebSocket connections, receives messages, and broadcasts them to every connected client in the same room. This is the core of real-time communication in Django Channels.

In this step, get ready to:
  • Create an async WebSocket consumer that handles connect, disconnect, and message events.
  • Set up WebSocket URL routing and update the ASGI configuration.
  • Test real-time messaging between two browser tabs.
Create an async WebSocket consumer

A consumer is the Django Channels equivalent of a view, but for WebSocket connections instead of HTTP requests. It defines what happens when a client connects, sends a message, or disconnects.

  • Create a new file called consumers.py inside the chat folder.
  • Add the following code to chat/consumers.py:
import json

from channels.generic.websocket import AsyncWebsocketConsumer


class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        # Extract the room name from the WebSocket URL
        self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
        # Build a group name for this room's channel layer group
        self.room_group_name = f"chat_{self.room_name}"

        # Join the room group so this consumer receives broadcasts
        await self.channel_layer.group_add(self.room_group_name, self.channel_name)

        # Accept the WebSocket connection
        await self.accept()

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

    async def receive(self, text_data):
        # Parse the incoming JSON message from the client
        text_data_json = json.loads(text_data)
        message = text_data_json["message"]

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

    async def chat_message(self, event):
        # Called when a group_send event with type "chat.message" arrives
        message = event["message"]

        # Send the message to the WebSocket client
        await self.send(text_data=json.dumps({"message": message}))

What does this code do?

This consumer manages the full lifecycle of a WebSocket connection using four methods:

  • connect() extracts the room name from the URL, builds a group name like chat_lobby, joins that group via the channel layer, and accepts the connection.
  • disconnect() removes the consumer from the group when the client closes the tab.
  • receive() is called when a client sends a message. It parses the JSON and uses group_send to broadcast it to every consumer in the room.
  • chat_message() handles the broadcast event and sends the message down the WebSocket to the individual client.

Why does chat_message match the type field?

When group_send dispatches an event with "type": "chat.message", Django Channels converts the dot to an underscore and calls the method named chat_message. This naming convention is how consumers route internal events to the correct handler.

  • Save chat/consumers.py.

You will not see a visible change yet. The next chunk creates the routing that connects WebSocket URLs to this consumer.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

import json

from channels.generic.websocket import AsyncWebsocketConsumer


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

        await self.channel_layer.group_add(self.room_group_name, self.channel_name)

        await self.accept()

    async def disconnect(self, close_code):
        await self.channel_layer.group_discard(self.room_group_name, self.channel_name)

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

        await self.channel_layer.group_send(
            self.room_group_name, {"type": "chat.message", "message": message}
        )

    async def chat_message(self, event):
        message = event["message"]

        await self.send(text_data=json.dumps({"message": message}))
Set up WebSocket routing

Django uses URL patterns to route HTTP requests to views. Similarly, Django Channels uses a routing file to map WebSocket URLs to consumers.

  • Create a new file called routing.py inside the chat folder.
  • Add the following code to chat/routing.py:
from django.urls import re_path

from . import consumers

# Map WebSocket URLs to the consumer class
websocket_urlpatterns = [
    re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi()),
]

What does this code do?

This maps any WebSocket connection to ws/chat/<room_name>/ to your ChatConsumer. The regex captures the room name from the URL, which the consumer reads from self.scope["url_route"]["kwargs"]["room_name"].

The .as_asgi() call wraps the consumer class as an ASGI application, similar to how .as_view() works for Django class-based views.

  • Save chat/routing.py.

You will not see a change yet. The next step connects this routing to the main ASGI configuration.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

from django.urls import re_path

from . import consumers

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

Now you need to wire this routing into your project's ASGI configuration so Daphne knows to route WebSocket connections through it.

  • Open chatproject/asgi.py in your editor.
  • Replace the entire contents of the file with the following code:
import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application

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

# Initialize Django ASGI app before importing app modules
django_asgi_app = get_asgi_application()

from chat.routing import websocket_urlpatterns

# Route HTTP to Django and WebSocket to our consumer via URL routing
application = ProtocolTypeRouter(
    {
        "http": django_asgi_app,
        "websocket": AllowedHostsOriginValidator(
            AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
        ),
    }
)

What does this code do?

  • ProtocolTypeRouter inspects each incoming connection. HTTP requests go to Django's standard ASGI app. WebSocket connections go through the URLRouter which matches them to your consumer.
  • AllowedHostsOriginValidator checks the WebSocket Origin header against your ALLOWED_HOSTS setting for security.
  • AuthMiddlewareStack populates scope["user"] so your consumer can access the authenticated user if needed.
  • The from chat.routing import websocket_urlpatterns import is placed AFTER get_asgi_application() because Django apps must be initialized before you can import app modules.
  • Save chatproject/asgi.py.
  • Return to your terminal and stop the running server with Ctrl+C.
  • Restart the server by running:
python manage.py runserver

You should see Starting ASGI/Daphne in the terminal output, confirming that Daphne is serving both HTTP and WebSocket connections.

Seeing an import error?

  • Check that chat/routing.py exists and is saved. The asgi.py file imports from it.
  • Make sure the from chat.routing import websocket_urlpatterns line comes after django_asgi_app = get_asgi_application(). Reversing the order causes Django to fail before apps are loaded.
  • Confirm your redis-chat Docker container is still running with docker ps. If it stopped, run docker start redis-chat.

Why is my Django Channels server failing to start?

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application

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

django_asgi_app = get_asgi_application()

from chat.routing import websocket_urlpatterns

application = ProtocolTypeRouter(
    {
        "http": django_asgi_app,
        "websocket": AllowedHostsOriginValidator(
            AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
        ),
    }
)
Test the real-time chat

Everything is wired up. Time to prove that messages broadcast in real time across multiple clients.

  • Open your browser and navigate to http://127.0.0.1:8000/chat/.
  • Type lobby into the room name field and click Join.
  • Open a second browser tab and navigate to the same URL: http://127.0.0.1:8000/chat/lobby/.
  • In either tab, type a message and click Send.

The message appears instantly in both tabs. Your Redis-backed channel layer is broadcasting the message to every consumer in the group.

Messages not appearing in both tabs?

  • Open your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows). If you see "Chat socket closed unexpectedly", the WebSocket connection failed.
  • Confirm your redis-chat container is running by checking docker ps in a new terminal. Without Redis, the channel layer cannot broadcast.
  • Check the terminal running your Django server for error messages. A traceback mentioning ConnectionRefusedError means Redis is not reachable on port 6379.

Help me debug why messages are not broadcasting between my browser tabs.

Secret mission

Add Usernames and Message Persistence

Your chat works in real time, but messages vanish when the page reloads. Can you add usernames to each message and save them to the database so new users see the conversation history when they join a room?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Docker container redis-chat running Redis on port 6379.
  • Python virtual environment venv with Django, Channels, Daphne, and channels-redis packages installed.
  • Local project files (chatproject/, chat/, manage.py, db.sqlite3).

Keep everything running

No action needed. Choose this if you're still actively building or experimenting with your chat application.

  • Your redis-chat Docker container continues running in the background.
  • Your virtual environment stays active for continued development.
  • Your project files remain on your Desktop, ready for future changes.

Pause - I'll come back to this later

Stop the Docker container to free up resources. You can restart it later without losing any configuration.

  • Stop the Redis container by running:
docker stop redis-chat
  • Deactivate your virtual environment by running:
deactivate

When you're ready to come back, restart the container and reactivate your environment:

docker start redis-chat
venv\Scripts\activate
  • Then start the development server with python manage.py runserver and pick up where you left off.

Delete - I don't want to use this again

Remove all project resources and start fresh. This permanently deletes your chat application and its data.

  • Remove the Redis Docker container by running:
docker rm -f redis-chat
  • Deactivate your virtual environment by running:
deactivate
  • Navigate to your Desktop and delete the project folder (the folder containing manage.py and chatproject/).

macOS

cd ~/Desktop
rm -rf chatproject

Windows

cd ~\Desktop
rmdir /s /q chatproject

Your machine is now clean. No costs were incurred since everything ran locally.

Nice Work!

Nice Work!

You just built a real-time multi-room chat server using Django Channels and WebSockets. Messages fly between browser tabs instantly, powered by an async consumer and Redis-backed channel layers.

You've learned how to:

  • Write an async WebSocket consumer with Django Channels that handles the full connection lifecycle (connect, disconnect, receive, and broadcast).
  • Configure Django for ASGI with Daphne and use Redis-backed channel layers to broadcast messages across all consumers in a room.
  • Connect a JavaScript frontend to a WebSocket endpoint for instant message delivery without page refreshes.
  • Secret Mission: Added usernames and message persistence using database_sync_to_async so new users see chat history on page load.

Ready to quiz yourself?