Kalau kamu pernah coba bikin fitur chat di Django pakai AJAX polling, kamu pasti tahu betapa borosnya pola itu: browser menembak server tiap dua detik, dan mayoritas responsnya cuma berisi "tidak ada pesan baru". WebSocket menghapus semua pemborosan itu. Satu koneksi dua arah yang tetap terbuka, jadi server bisa mendorong data kapan saja tanpa ditanya. Masalahnya, Django standar berjalan di WSGI, protokol sinkron yang tidak mengenal WebSocket. Di sinilah Django Channels masuk. Ia membawa ASGI ke Django sehingga kamu bisa menangani WebSocket dan broadcast antar koneksi. Di akhir tulisan ini kamu akan punya chat room yang benar-benar jalan, lengkap dengan Redis sebagai backend broadcast-nya.
Siapkan Project
Mulai dari project kosong. Kalau kamu belum pernah menyentuh Django sama sekali, baca dulu membuat website pertamamu dengan Django, lalu kembali ke sini. Kita butuh tiga package: channels (inti WebSocket), daphne (server ASGI), dan channels-redis (dipakai nanti untuk broadcast lintas proses).
python -m venv .venv
source .venv/bin/activate
pip install django "channels[daphne]" channels-redis
django-admin startproject chatproject .
python manage.py startapp chat
Aktifkan Channels dan Atur Channel Layer
Buka chatproject/settings.py dan lakukan tiga hal: taruh daphne di posisi paling atas INSTALLED_APPS (harus paling atas supaya runserver memakai ASGI, bukan WSGI), daftarkan app chat dan channels, lalu set ASGI_APPLICATION dan CHANNEL_LAYERS.
# chatproject/settings.py
INSTALLED_APPS = [
"daphne", # harus paling atas
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"channels",
"chat",
]
ASGI_APPLICATION = "chatproject.asgi.application"
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer",
},
}
InMemoryChannelLayer cuma untuk development. Pesan tidak bertahan saat server restart, dan ia tidak bisa berbagi pesan antar proses. Kalau nanti kamu jalankan banyak worker, koneksi di worker A tidak akan menerima broadcast dari worker B. Kita ganti dengan Redis di bagian akhir.
Consumer: Tempat Koneksi WebSocket Ditangani
Consumer adalah inti Django Channels. Ia punya tiga momen penting: connect (saat koneksi dibuka), receive (saat pesan masuk), dan disconnect (saat koneksi ditutup). Kita pakai AsyncWebsocketConsumer supaya bisa await. Konsepnya mirip dengan endpoint WebSocket di FastAPI yang sudah saya bahas sebelumnya, bedanya di sini semuanya lewat ASGI Django.
# chat/consumers.py
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):
data = json.loads(text_data)
await self.channel_layer.group_send(
self.room_group_name,
{
"type": "chat.message",
"username": data["username"],
"message": data["message"],
},
)
async def chat_message(self, event):
await self.send(
text_data=json.dumps(
{"username": event["username"], "message": event["message"]}
)
)
Perhatikan receive tidak mengirim langsung ke klien. Ia memanggil group_send ke room_group_name. Channel layer lalu memanggil method chat_message di setiap consumer yang tergabung dalam grup itu — nama method-nya diambil dari nilai type, dengan titik diganti garis bawah (chat.message jadi chat_message). Baru di situ pesan benar-benar dikirim ke tiap klien. Pola inilah yang membuat satu pesan sampai ke semua orang dalam satu room.
Routing WebSocket
Consumer butuh peta URL. Buat chat/routing.py yang memetakan path WebSocket ke consumer. Perhatikan path-nya memakai re_path dengan regex, bukan path biasa.
# 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()),
]
asgi.py: Router Protokol
Terakhir di sisi server, ganti isi chatproject/asgi.py. Router ini membaca jenis koneksi yang masuk: kalau HTTP, teruskan ke aplikasi Django biasa; kalau WebSocket, teruskan ke URLRouter yang kita buat barusan.
# chatproject/asgi.py
import os
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
import chat.routing
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chatproject.settings")
application = ProtocolTypeRouter(
{
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(URLRouter(chat.routing.websocket_urlpatterns)),
}
)
View, URL, dan Template
Sekarang sisi HTTP: satu view yang merender halaman chat, satu URL, dan satu template berisi JavaScript yang membuka koneksi WebSocket. Buat folder chat/templates/chat/ lalu simpan file room.html di dalamnya.
# chat/views.py
from django.shortcuts import render
def room(request, room_name):
return render(request, "chat/room.html", {"room_name": room_name})
# chatproject/urls.py
from django.contrib import admin
from django.urls import path
from chat import views
urlpatterns = [
path("admin/", admin.site.urls),
path("chat/<str:room_name>/", views.room, name="room"),
]
<!-- chat/templates/chat/room.html -->
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Chat {{ room_name }}</title>
</head>
<body>
<h2>Room: {{ room_name }}</h2>
<div id="chat-log" style="font-family: monospace;"></div>
<input id="username" placeholder="Nama kamu">
<input id="message-input" placeholder="Pesan...">
<button id="send">Kirim</button>
<script>
const roomName = "{{ room_name }}";
const socket = new WebSocket(`ws://${window.location.host}/ws/chat/${roomName}/`);
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
const line = document.createElement("p");
line.textContent = `${data.username}: ${data.message}`;
document.getElementById("chat-log").appendChild(line);
};
document.getElementById("send").onclick = () => {
const username = document.getElementById("username").value || "anon";
const message = document.getElementById("message-input").value;
socket.send(JSON.stringify({ username, message }));
document.getElementById("message-input").value = "";
};
</script>
</body>
</html>
Jalankan dan Tes
Karena daphne sudah terpasang di INSTALLED_APPS, perintah runserver otomatis menjalankan server ASGI yang bisa menangani WebSocket. Jalankan, lalu buka dua tab browser di alamat yang sama — misalnya http://127.0.0.1:8000/chat/umum/. Ketik pesan di satu tab, dan pesan itu langsung muncul di tab satunya tanpa reload.
python manage.py runserver
Redis untuk Broadcast Lintas Proses
Di development, satu proses saja sudah cukup. Masalah muncul saat kamu deploy dengan beberapa worker: InMemoryChannelLayer menyimpan pesan di memori masing-masing proses, jadi broadcast dari satu worker tidak akan sampai ke koneksi yang dipegang worker lain. Solusinya adalah memakai Redis sebagai bus pesan bersama. Kalau kamu belum kenal Redis, saya sudah membahasnya di Belajar Redis dengan Python.
docker run -d --name redis-chat -p 6379:6379 redis:7
pip install channels-redis
# chatproject/settings.py — ganti CHANNEL_LAYERS
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6379)],
},
},
}
Untuk produksi, jangan jalankan lewat runserver. Pakai daphne atau uvicorn di belakang Nginx, dan pastikan Redis-nya juga production-ready (persistence, password). runserver hanya untuk development.
Langkah Berikutnya
Chat room ini masih menyimpan pesan di memori — begitu server restart, riwayat hilang. Langkah berikutnya yang paling berguna: simpan tiap pesan ke database lewat ORM Django, lalu tampilkan 50 pesan terakhir saat halaman pertama dibuka. Setelah itu, cari satu halaman di project-mu yang masih pakai polling atau auto-refresh, dan ganti dengan WebSocket. Begitu kamu merasakan bedanya, kamu tidak akan kembali ke polling.