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"],
})
typecon punto engroup_sendse 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.1y los headersUpgrade/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) ymaxmemory-policy: allkeys-lrupor seguridad.
Para volumenes muy altos (>10k conexiones simultaneas) considera Redis Streams o un broker dedicado tipo NATS / Centrifugo.
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