Back to Scaling Python Applications guides

Getting Started with Django WebSockets

Stanley Ulili
Updated on August 19, 2025

Django has been a popular choice in Python for creating strong and reliable web applications for a long time. However, when it comes to real-time features, its traditional request-response cycle can feel limiting.

That's where Django Channels comes in. It enhances Django to support WebSockets, making it easier to develop engaging and interactive features like live notifications, real-time collaboration tools, and instant messaging systems.

This comprehensive guide will take you through implementing WebSocket functionality in Django from the ground up.

Prerequisites

Before you start exploring Django WebSocket development, make sure you have Python 3.8 or newer installed. Having a good grasp of Django basics, like models, views, and URL routing, will be very helpful. It’s also beneficial to have basic JavaScript skills as we work through our interactive examples together.

Setting up your Django Channels project

To ensure practical real-time applications, we will set up a well-organized workspace that distinctly separates WebSocket logic from standard Django components.

Beginning with a proper virtual environment allows you to implement Channels correctly from the outset.

 
mkdir django_websockets && cd django_websockets
 
python3 -m venv venv
 
source venv/bin/activate

These commands create your project directory, set up an isolated virtual environment, and activate it to keep your dependencies separate from other projects.

Now install Django and Channels for WebSocket support:

 
pip install django channels

With Django installed, create your Django project:

 
django-admin startproject config .

Create a dedicated app for your WebSocket functionality:

 
python manage.py startapp realtime

The config directory contains your project settings while the realtime app will house all WebSocket-related functionality, keeping your code organized and maintainable.

Configure Django to recognize Channels by updating your settings. Add this to your config/settings.py:

config/settings.py
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
'channels', # Add Channels
'realtime', # Your WebSocket app
] ...
# Channels configuration
ASGI_APPLICATION = 'config.asgi.application'
# For development, use in-memory backend
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer'
}
}

The ASGI_APPLICATION setting tells Django to use your ASGI configuration for handling both HTTP and WebSocket connections. The channel layer configuration enables communication between different consumers and background tasks. The in-memory backend works perfectly for development and testing.

Update your ASGI configuration file at config/asgi.py:

config/asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from realtime.routing import websocket_urlpatterns
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
application = ProtocolTypeRouter({
'http': get_asgi_application(),
'websocket': AuthMiddlewareStack(
URLRouter(
websocket_urlpatterns
)
),
})

This configuration creates a protocol router that intelligently directs HTTP requests to Django's standard handler and WebSocket connections to your custom routing. The authentication middleware ensures WebSocket connections have full access to Django's user system, enabling features like user identification and permission checking.

Set up WebSocket routing by creating realtime/routing.py:

realtime/routing.py
from django.urls import re_path
from . import consumers

websocket_urlpatterns = [
    re_path(r'ws/chat/$', consumers.ChatConsumer.as_asgi()),
]

WebSocket routing works similarly to Django's URL routing but uses different patterns. The r'ws/chat/$' pattern will match WebSocket connections to /ws/chat/, while the .as_asgi() method converts your consumer class to work with ASGI.

Building your first WebSocket consumer and interface

Now that your project structure is ready, let's create the consumer that handles WebSocket connections and build a polished interface to test it.

Create a basic consumer to handle WebSocket connections in realtime/consumers.py:

realtime/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        await self.accept()
        print(f'WebSocket connected: {self.channel_name}')

    async def disconnect(self, close_code):
        print(f'WebSocket disconnected: {self.channel_name}')

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

        # Echo message back to client
        await self.send(text_data=json.dumps({
            'message': f'Echo: {message}',
            'sender': 'system'
        }))

This basic consumer inherits from AsyncWebsocketConsumer for optimal performance with async/await patterns. The connect method accepts incoming connections, disconnect handles cleanup when connections close, and receive processes incoming messages. The echo functionality confirms your WebSocket setup is working correctly.

Create a simple view to serve your test interface in realtime/views.py:

realtime/views.py
from django.shortcuts import render

def index(request):
    return render(request, 'realtime/index.html')

Add the view to your main URL configuration in config/urls.py:

config/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [ path('admin/', admin.site.urls),
path('', include('realtime.urls')),
]

Create realtime/urls.py:

realtime/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
]

This URL configuration serves your chat interface at the root path while keeping WebSocket routing separate in the routing.py file.

Now create the template directory and your test interface:

 
mkdir -p realtime/templates/realtime

Create your WebSocket client interface at realtime/templates/realtime/index.html:

realtime/templates/realtime/index.html
<!DOCTYPE html>
<html>
<head>
    <title>Django WebSocket Demo</title>
    <style>
        body { 
            font-family: system-ui, -apple-system, sans-serif; 
            max-width: 800px; 
            margin: 0 auto; 
            padding: 20px; 
            background-color: #f5f5f5;
        }
        .chat-container {
            background: white;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            overflow: hidden;
        }
        .chat-header {
            background: #2563eb;
            color: white;
            padding: 15px 20px;
            font-weight: 600;
        }
        #chat-messages { 
            height: 400px; 
            overflow-y: auto; 
            padding: 20px;
            border-bottom: 1px solid #e5e7eb;
        }
        .message {
            margin-bottom: 10px;
            padding: 8px 12px;
            background: #f3f4f6;
            border-radius: 6px;
            max-width: 70%;
        }
        .message.own {
            background: #3b82f6;
            color: white;
            margin-left: auto;
            text-align: right;
        }
        .input-container {
            padding: 20px;
            display: flex;
            gap: 10px;
        }
        #message-input { 
            flex: 1;
            padding: 12px;
            border: 1px solid #d1d5db;
            border-radius: 6px;
            font-size: 16px;
        }
        #send-button { 
            padding: 12px 24px;
            background: #2563eb;
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-weight: 500;
        }
        #send-button:hover {
            background: #1d4ed8;
        }
        .status {
            padding: 10px 20px;
            background: #f3f4f6;
            color: #6b7280;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <div class="chat-container">
        <div class="chat-header">
            <h1 style="margin: 0;">Django WebSocket Chat</h1>
        </div>
        <div class="status" id="status">Connecting...</div>
        <div id="chat-messages"></div>
        <div class="input-container">
            <input type="text" id="message-input" placeholder="Type your message..." maxlength="500">
            <button id="send-button" disabled>Send</button>
        </div>
    </div>

    <script>
        const chatMessages = document.querySelector('#chat-messages');
        const messageInput = document.querySelector('#message-input');
        const sendButton = document.querySelector('#send-button');
        const status = document.querySelector('#status');

        const chatSocket = new WebSocket(
            'ws://' + window.location.host + '/ws/chat/'
        );

        chatSocket.onopen = function(e) {
            status.textContent = 'Connected to chat server';
            status.style.backgroundColor = '#d1fae5';
            status.style.color = '#065f46';
            sendButton.disabled = false;
            messageInput.focus();
        };

        chatSocket.onmessage = function(e) {
            const data = JSON.parse(e.data);
            const messageElement = document.createElement('div');
            messageElement.classList.add('message');
            messageElement.textContent = data.message;

            if (data.sender === 'self') {
                messageElement.classList.add('own');
            }

            chatMessages.appendChild(messageElement);
            chatMessages.scrollTop = chatMessages.scrollHeight;
        };

        chatSocket.onclose = function(e) {
            status.textContent = 'Disconnected from chat server';
            status.style.backgroundColor = '#fee2e2';
            status.style.color = '#991b1b';
            sendButton.disabled = true;
        };

        chatSocket.onerror = function(e) {
            status.textContent = 'Connection error occurred';
            status.style.backgroundColor = '#fee2e2';
            status.style.color = '#991b1b';
        };

        function sendMessage() {
            const message = messageInput.value.trim();
            if (message && chatSocket.readyState === WebSocket.OPEN) {
                chatSocket.send(JSON.stringify({
                    'message': message
                }));
                messageInput.value = '';
            }
        }

        sendButton.addEventListener('click', sendMessage);

        messageInput.addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                sendMessage();
            }
        });
    </script>
</body>
</html>

This interface provides a polished chat experience with modern styling, connection status indicators, and smooth message handling. The WebSocket connection automatically manages the connection lifecycle, provides visual feedback through status colors, and includes keyboard shortcuts for better usability. The JavaScript creates a WebSocket connection to your Django consumer and handles all message events with proper error management.

Run the initial migration and start your development server:

 
python manage.py migrate
 
python manage.py runserver

Visit http://localhost:8000 to see your chat interface. Initially, you'll see a disconnected status since we have a basic consumer that needs enhancement:

Screenshot of Django WebSocket chat interface showing initial disconnected state

The red status indicates the WebSocket connection isn't being handled properly yet. In the next section, we'll enhance the consumer to establish a proper connection and implement the echo functionality.

Creating your first WebSocket consumer

Django Channels uses "consumers" as the WebSocket equivalent of Django views. These consumers handle connection lifecycle events and message processing, giving you full control over real-time interactions while maintaining Django's familiar patterns.

Let's enhance your consumer to properly handle WebSocket connections and implement echo functionality. Update your realtime/consumers.py:

realtime/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
# Accept the WebSocket connection
await self.accept()
# Log the connection
print(f'WebSocket connected: {self.channel_name}')
# Send welcome message
await self.send(text_data=json.dumps({
'message': 'Welcome to Django WebSocket chat!',
'sender': 'system'
}))
async def disconnect(self, close_code):
print(f'WebSocket disconnected: {self.channel_name} (code: {close_code})')
async def receive(self, text_data):
try:
# Parse incoming message
text_data_json = json.loads(text_data)
message = text_data_json['message']
print(f'Received message: {message}')
# Echo the message back
await self.send(text_data=json.dumps({
'message': f'Echo: {message}',
'sender': 'system'
}))
except json.JSONDecodeError:
# Handle invalid JSON
await self.send(text_data=json.dumps({
'message': 'Invalid message format',
'sender': 'system'
}))
except Exception as e:
print(f'Error processing message: {e}')

The enhanced consumer now properly accepts WebSocket connections with await self.accept(), which is essential for establishing the connection. The connect method runs when clients establish connections and sends a welcome message. The receive method processes incoming messages with proper JSON parsing and error handling, while disconnect handles cleanup when connections close.

Each consumer instance represents a single WebSocket connection, identified by a unique channel_name. This architecture allows you to manage individual connections while enabling group communication features later.

For WebSocket functionality to work properly, you need to run Django with an ASGI server instead of the default WSGI server. Install Daphne, Django Channels' recommended ASGI server:

 
pip install daphne

Start your server using the ASGI protocol:

 
daphne -b 127.0.0.1 -p 8000 config.asgi:application

You should see output indicating the ASGI server is running:

Output
2025-08-19 12:09:21,989 INFO     Starting server at tcp:port=8000:interface=127.0.0.1
2025-08-19 12:09:21,989 INFO     HTTP/2 support not enabled (install the http2 and tls Twisted extras)
2025-08-19 12:09:21,989 INFO     Configuring endpoint tcp:port=8000:interface=127.0.0.1
2025-08-19 12:09:21,989 INFO     Listening on TCP address 127.0.0.1:8000

Return to http://localhost:8000 and you'll see the connection status change to green:

Screenshot of Django WebSocket chat interface showing successful connection

The interface now shows "Connected to chat server" in green, and you should see the welcome message appear automatically. The send button is now enabled, indicating a successful WebSocket connection.

Screenshot of Django WebSocket chat interface showing successful connection with welcome message

Type a message and press Enter to test the echo functionality:

Screenshot showing echo message functionality working in Django WebSocket chat

Your message will be echoed back with an "Echo:" prefix, confirming that your consumer is properly receiving and sending messages through the WebSocket connection. The basic chat functionality is now working perfectly!

Your Django WebSocket consumer is successfully handling connections and messages.

Broadcasting messages to multiple clients

Real WebSocket applications typically need to send messages to multiple clients simultaneously. Your current echo server only responds to individual senders. Let's build message broadcasting functionality to enable multi-client communication.

Create a simple connection manager to track and message multiple clients:

realtime/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer

# Simple connection manager
connections = set()
def broadcast(message, sender=None):
for connection in connections.copy():
if connection != sender:
try:
connection.send(text_data=json.dumps({
'message': message,
'sender': 'system'
}))
except:
connections.discard(connection)
class ChatConsumer(AsyncWebsocketConsumer): async def connect(self): await self.accept()
# Add to connection pool
connections.add(self)
print(f'Client connected. Total connections: {len(connections)}')
# Welcome message with connection count
await self.send(text_data=json.dumps({
'message': f'Welcome! {len(connections)} clients connected.',
'sender': 'system'
}))
# Notify other clients
for connection in connections:
if connection != self:
try:
await connection.send(text_data=json.dumps({
'message': f'New user joined. {len(connections)} total users.',
'sender': 'system'
}))
except:
connections.discard(connection)
async def disconnect(self, close_code):
connections.discard(self)
print(f'Client disconnected. Remaining connections: {len(connections)}')
# Notify remaining clients
for connection in connections:
try:
await connection.send(text_data=json.dumps({
'message': f'User left. {len(connections)} total users.',
'sender': 'system'
}))
except:
connections.discard(connection)
async def receive(self, text_data): try: text_data_json = json.loads(text_data) message = text_data_json['message']
print(f'Broadcasting message: {message}')
# Broadcast message to all other clients
for connection in connections:
if connection != self:
try:
await connection.send(text_data=json.dumps({
'message': f'User says: {message}',
'sender': 'other'
}))
except:
connections.discard(connection)
except json.JSONDecodeError: await self.send(text_data=json.dumps({ 'message': 'Invalid message format', 'sender': 'system' }))

The connections set tracks all active WebSocket connections. When clients connect, they get a personalized welcome message, and other clients are informed. Messages sent by any client are broadcasted to everyone else, forming a simple chat system.

Update your client interface to handle the new message types:

realtime/templates/realtime/index.html
<!-- Update just the JavaScript section in your existing HTML -->
<script>
// ... keep existing code until onmessage ...
chatSocket.onopen = function (e) {
...
};
chatSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
const messageElement = document.createElement('div');
messageElement.classList.add('message');
messageElement.textContent = data.message;
if (data.sender === 'other') {
// Messages from other users
messageElement.style.backgroundColor = '#f3f4f6';
} else if (data.sender === 'system') {
// System notifications
messageElement.style.backgroundColor = '#fef3c7';
messageElement.style.fontStyle = 'italic';
messageElement.style.textAlign = 'center';
}
chatMessages.appendChild(messageElement);
chatMessages.scrollTop = chatMessages.scrollHeight;
};
// ... keep rest of existing code ... </script>

Restart your Daphne server:

 
daphne -b 127.0.0.1 -p 8000 config.asgi:application

Open multiple browser tabs to http://localhost:8000. You'll see connection notifications and user counts update in real-time:

Screenshot showing multiple welcome messages and user join notifications

Send a message from one tab and see it appear instantly in all the other tabs:

Screenshot of message broadcasting across multiple clients

You've successfully built a real-time multi-client chat system with Django WebSockets!

Final thoughts

This guide walked you through building a real-time chat system with Django Channels, starting from basic WebSocket connections and progressing to multi-client message broadcasting with proper connection management.

For production deployment, consider implementing user authentication, message persistence with Django models, and scaling with Redis channel layers for multiple server instances. The Django Channels documentation provides detailed guidance on these advanced topics, while Django's deployment checklist covers security considerations for real-time applications.

The foundation you've built here scales naturally to support features like private messaging, file sharing, live notifications, and collaborative editing, making Django Channels an excellent choice for modern interactive web applications.

Got an article suggestion? Let us know
Next article
Get Started with Job Scheduling in Python
Learn how to create and monitor Python scheduled tasks in a production environment
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.