WebSockets en DRF con Django Channels

Avanzado
Django
Django
Actualizado: 19/04/2026

Por que WebSockets y donde usarlos

Una API REST clasica funciona bien para CRUD pero falla en escenarios donde el servidor necesita empujar datos sin que el cliente los pida. Hacer polling cada 2 segundos consume bateria, ancho de banda y carga inutil al backend.

Casos donde WebSockets son la opcion correcta:

  • Chat en vivo (mensajes entre usuarios, salas).
  • Notificaciones push dentro de la app web (ticket actualizado, pedido enviado).
  • Dashboards en tiempo real (metricas, sensores IoT, precios bursatiles).
  • Colaboracion multiusuario (editores tipo Google Docs, kanban en vivo).
  • Streaming de progreso (job en cola que reporta su avance al cliente).

Django solo no soporta WebSockets. Necesitas Django Channels, que extiende Django para ASGI y anade el concepto de consumer: el equivalente a una vista pero para conexiones persistentes.

Instalacion y configuracion

pip install channels channels-redis
# settings.py
INSTALLED_APPS = [
    "daphne",          # debe ir el primero en INSTALLED_APPS
    "django.contrib.contenttypes",
    "django.contrib.auth",
    "channels",
    "rest_framework",
    "chat",
]

ASGI_APPLICATION = "core.asgi.application"

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

El Channel Layer es la pieza clave. Es Redis (o IPC) compartido entre todos los workers, lo que permite que un mensaje publicado por un worker llegue a clientes conectados en otro worker. Sin el, cada proceso solo veria a sus propios clientes.

ASGI con HTTP + WebSocket

# core/asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from chat.middleware import JWTAuthMiddleware
from chat.routing import websocket_urlpatterns

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
django_asgi_app = get_asgi_application()

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

ProtocolTypeRouter decide segun el protocolo: las peticiones HTTP siguen yendo al stack DRF normal, las WebSocket pasan por nuestro middleware JWT y luego al router de consumers.

Routing y primer consumer

# chat/routing.py
from django.urls import re_path
from chat.consumers import ChatConsumer

websocket_urlpatterns = [
    re_path(r"^ws/sala/(?P<sala_id>[\w-]+)/$", ChatConsumer.as_asgi()),
]
# chat/consumers.py
from channels.generic.websocket import AsyncJsonWebsocketConsumer

class ChatConsumer(AsyncJsonWebsocketConsumer):
    async def connect(self):
        self.user = self.scope["user"]
        if self.user.is_anonymous:
            await self.close(code=4401)
            return

        self.sala_id = self.scope["url_route"]["kwargs"]["sala_id"]
        self.group   = f"sala_{self.sala_id}"

        await self.channel_layer.group_add(self.group, self.channel_name)
        await self.accept()

    async def disconnect(self, code):
        await self.channel_layer.group_discard(self.group, self.channel_name)

    async def receive_json(self, content, **kwargs):
        texto = content.get("texto", "").strip()
        if not texto:
            return
        # Reemite a toda la sala
        await self.channel_layer.group_send(self.group, {
            "type":    "chat.message",  # invoca self.chat_message
            "texto":   texto,
            "autor":   self.user.username,
        })

    async def chat_message(self, event):
        await self.send_json({
            "texto": event["texto"],
            "autor": event["autor"],
        })

type con punto en group_send se traduce al metodo del consumer reemplazando el punto por guion bajo (chat.message -> chat_message).

Autenticacion JWT en el handshake

WebSockets no tienen cookies utiles ni headers personalizados en navegadores estandar. La forma practica es enviar el JWT en el query string.

# chat/middleware.py
from urllib.parse import parse_qs
from channels.db import database_sync_to_async
from channels.middleware import BaseMiddleware
from django.contrib.auth.models import AnonymousUser
from rest_framework_simplejwt.tokens import UntypedToken
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from django.contrib.auth import get_user_model

User = get_user_model()

@database_sync_to_async
def _get_user(user_id):
    try:
        return User.objects.get(id=user_id)
    except User.DoesNotExist:
        return AnonymousUser()

class JWTAuthMiddleware(BaseMiddleware):
    async def __call__(self, scope, receive, send):
        token = parse_qs(scope["query_string"].decode()).get("token", [None])[0]
        scope["user"] = AnonymousUser()
        if token:
            try:
                payload = UntypedToken(token)
                scope["user"] = await _get_user(payload["user_id"])
            except (InvalidToken, TokenError):
                pass
        return await super().__call__(scope, receive, send)

Cliente JavaScript:

const token = localStorage.getItem("access");
const ws = new WebSocket(
    `wss://api.miapp.com/ws/sala/general/?token=${token}`
);
ws.onmessage = (e) => console.log(JSON.parse(e.data));
ws.send(JSON.stringify({ texto: "Hola" }));

En produccion siempre wss:// (TLS). Sin TLS, el token viaja en claro y queda expuesto a sniffing.

Disparar mensajes WS desde una APIView REST

Patron muy frecuente: tu API REST recibe un POST y necesita avisar a varios clientes via WebSocket.

# tickets/views.py
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from rest_framework.viewsets import ModelViewSet

class TicketViewSet(ModelViewSet):
    queryset = Ticket.objects.all()
    serializer_class = TicketSerializer

    def perform_update(self, serializer):
        ticket = serializer.save()
        layer = get_channel_layer()
        async_to_sync(layer.group_send)(
            f"ticket_{ticket.id}",
            {
                "type":   "ticket.updated",
                "estado": ticket.estado,
                "id":     str(ticket.id),
            },
        )

Y el consumer correspondiente:

class TicketConsumer(AsyncJsonWebsocketConsumer):
    async def connect(self):
        self.ticket_id = self.scope["url_route"]["kwargs"]["ticket_id"]
        await self.channel_layer.group_add(
            f"ticket_{self.ticket_id}", self.channel_name
        )
        await self.accept()

    async def ticket_updated(self, event):
        await self.send_json({"id": event["id"], "estado": event["estado"]})

Diagrama de flujo

sequenceDiagram
    participant C1 as Cliente A (WS)
    participant C2 as Cliente B (WS)
    participant W1 as Worker 1
    participant W2 as Worker 2
    participant R as Redis (Channel Layer)
    participant API as REST API
    C1->>W1: WS connect /ws/ticket/42/
    C2->>W2: WS connect /ws/ticket/42/
    W1->>R: group_add ticket_42
    W2->>R: group_add ticket_42
    API->>W1: PUT /api/tickets/42/ {estado: pagado}
    W1->>R: group_send ticket_42 ticket.updated
    R->>W1: ticket_updated
    R->>W2: ticket_updated
    W1->>C1: send_json
    W2->>C2: send_json

Los dos clientes reciben el evento aunque esten conectados a workers distintos, gracias a Redis como Channel Layer.

Testing con WebsocketCommunicator

# chat/tests/test_consumers.py
import pytest
from channels.testing import WebsocketCommunicator
from core.asgi import application

@pytest.mark.asyncio
async def test_chat_envia_mensaje():
    com = WebsocketCommunicator(application, "/ws/sala/general/?token=ABC")
    connected, _ = await com.connect()
    assert connected
    await com.send_json_to({"texto": "Hola"})
    response = await com.receive_json_from()
    assert response["texto"] == "Hola"
    await com.disconnect()

Necesitas pytest-asyncio y un usuario real con un JWT valido (o un middleware mock para tests).

Despliegue

  • En produccion: uvicorn o daphne detras de nginx que hace proxy_pass con proxy_http_version 1.1 y los headers Upgrade/Connection.
  • Sticky sessions no son necesarias porque la Channel Layer (Redis) sincroniza grupos entre workers.
  • Configura CHANNEL_LAYERS["default"]["CONFIG"] con un Redis con persistencia desactivada (los mensajes WS son efimeros) y maxmemory-policy: allkeys-lru por seguridad.

Para volumenes muy altos (>10k conexiones simultaneas) considera Redis Streams o un broker dedicado tipo NATS / Centrifugo.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Django es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Django

Explora más contenido relacionado con Django y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Entender la arquitectura ASGI + Channels + Channel Layer Redis. Crear un AsyncJsonWebsocketConsumer con connect, disconnect, receive_json. Autenticar el WebSocket via token JWT en el query string. Anadir/retirar el cliente a un grupo y hacer group_send. Disparar mensajes WebSocket desde una APIView REST. Testear con WebsocketCommunicator y pytest-asyncio.

Cursos que incluyen esta lección

Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje