Construye un Chat Bidireccional con WebSocket y Node.js
Como construir una aplicacion de chat en tiempo real bidireccional usando WebSocket con mensajeria basada en salas, tracking de presencia y persistencia de mensajes
La comunicacion bidireccional permite tanto al cliente como al servidor enviar mensajes en cualquier momento. Una aplicacion de chat demuestra este patron perfectamente: los usuarios envian mensajes al servidor, que luego los difunde a otros participantes en la misma sala. WebSocket es el transporte ideal para esto porque mantiene una conexion persistente de baja latencia.
Cuando Usar Esto
- Necesitas mensajeria en tiempo real donde ambos lados pueden iniciar comunicacion
- Los indicadores de presencia y de escritura deben actualizarse instantaneamente
- El delivery de mensajes requiere acknowledgments y garantias de orden
Requisitos Previos
- Node.js 18+ con libreria
wso Socket.io - Redis para broadcast de mensajes multi-servidor
Solucion
1. Servidor WebSocket con Salas
// server/chat.ts
import { WebSocketServer, WebSocket } from 'ws';
import { createClient } from 'redis';
interface ChatClient extends WebSocket {
userId: string;
roomId: string;
}
const wss = new WebSocketServer({ port: 8080 });
const redis = createClient({ url: 'redis://localhost:6379' });
const rooms = new Map<string, Set<ChatClient>>();
wss.on('connection', (ws: ChatClient, req) => {
const url = new URL(req.url!, 'http://localhost');
ws.userId = url.searchParams.get('userId')!;
ws.roomId = url.searchParams.get('roomId')!;
joinRoom(ws);
broadcastPresence(ws.roomId);
ws.on('message', (data) => handleMessage(ws, data));
ws.on('close', () => leaveRoom(ws));
});
function joinRoom(client: ChatClient) {
if (!rooms.has(client.roomId)) {
rooms.set(client.roomId, new Set());
}
rooms.get(client.roomId)!.add(client);
client.send(JSON.stringify({
type: 'system',
content: `Unido a sala ${client.roomId}`,
}));
}
function leaveRoom(client: ChatClient) {
rooms.get(client.roomId)?.delete(client);
broadcastPresence(client.roomId);
}
function broadcastPresence(roomId: string) {
const clients = rooms.get(roomId);
if (!clients) return;
const users = Array.from(clients).map(c => c.userId);
const message = JSON.stringify({ type: 'presence', users });
clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
async function handleMessage(client: ChatClient, data: RawData) {
const payload = JSON.parse(data.toString());
if (payload.type === 'chat') {
const message = {
type: 'chat',
userId: client.userId,
content: payload.content,
timestamp: Date.now(),
id: generateId(),
};
// Persistir en base de datos
await saveMessage(client.roomId, message);
// Broadcast a sala
broadcastToRoom(client.roomId, message);
}
}
function broadcastToRoom(roomId: string, message: object) {
const clients = rooms.get(roomId);
if (!clients) return;
const data = JSON.stringify(message);
clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
}
2. Broadcast Multi-Servidor con Redis
// server/redis-broadcast.ts
const subscriber = redis.duplicate();
subscriber.subscribe('chat:messages', (err) => {
if (err) console.error('Error de suscripcion Redis:', err);
});
subscriber.on('message', (channel, message) => {
const payload = JSON.parse(message);
broadcastToRoom(payload.roomId, payload.message);
});
async function publishMessage(roomId: string, message: object) {
await redis.publish('chat:messages', JSON.stringify({ roomId, message }));
}
3. Cliente React con Reconexion
// hooks/useChat.ts
import { useEffect, useRef, useState, useCallback } from 'react';
export function useChat(roomId: string, userId: string) {
const [messages, setMessages] = useState<Message[]>([]);
const [users, setUsers] = useState<string[]>([]);
const [connected, setConnected] = useState(false);
const ws = useRef<WebSocket | null>(null);
const reconnectTimer = useRef<NodeJS.Timeout>();
const connect = useCallback(() => {
const socket = new WebSocket(
`wss://chat.example.com?roomId=${roomId}&userId=${userId}`
);
socket.onopen = () => {
setConnected(true);
if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'chat':
setMessages(prev => [...prev, data]);
break;
case 'presence':
setUsers(data.users);
break;
}
};
socket.onclose = () => {
setConnected(false);
// Reconectar con backoff exponencial
reconnectTimer.current = setTimeout(connect, 3000);
};
ws.current = socket;
}, [roomId, userId]);
useEffect(() => {
connect();
return () => {
ws.current?.close();
if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
};
}, [connect]);
const sendMessage = useCallback((content: string) => {
ws.current?.send(JSON.stringify({
type: 'chat',
content,
}));
}, []);
return { messages, users, connected, sendMessage };
}
4. Indicadores de Escritura
// server/typing.ts
const typingUsers = new Map<string, Set<string>>();
function handleTyping(client: ChatClient, isTyping: boolean) {
if (!typingUsers.has(client.roomId)) {
typingUsers.set(client.roomId, new Set());
}
const room = typingUsers.get(client.roomId)!;
if (isTyping) room.add(client.userId);
else room.delete(client.userId);
broadcastToRoom(client.roomId, {
type: 'typing',
users: Array.from(room),
});
}
Como Funciona
- Conexion WebSocket establece un canal full-duplex persistente
- Gestion de Salas agrupa conexiones por sala de chat para broadcast dirigido
- Redis Pub/Sub sincroniza mensajes entre multiples instancias de servidor
- Tracking de Presencia mantiene una lista actualizada de participantes activos
- Logica de Reconexion asegura que clientes se recuperen automaticamente de issues de red
Consideraciones de Produccion
- Usa Socket.io para reconexion automatica, heartbeat y manejo de salas
- Implementa rate limiting por usuario para prevenir spam
- Almacena mensajes en base de datos persistente con paginacion para historial
- Agrega encriptacion end-to-end para conversaciones sensitivas
Errores Comunes
- No manejar reconexion de WebSocket, causando que usuarios caigan en blips de red
- Broadcastear a todos los clientes conectados en lugar de filtrar por sala
- No persistir mensajes, llevando a perdida de datos en reinicio de servidor
Troubleshooting
- 5xx errors under load: check rate limits, connection pools, and downstream timeouts.
- CORS errors in the browser: confirm allowed origins, methods, and headers. Preflight requests must return the right headers before the actual request.
- Unexpected 404s: verify route definitions, path parameters, and base paths. Watch for trailing slashes and URL encoding differences.
- Authentication failures: validate token expiry, signature algorithms, and clock skew. Log rejected tokens without exposing secrets.
- Slow response times: profile the slowest percentiles.
Lectura Adicional
- Documentación oficial: consulta la referencia actualizada del framework o herramienta utilizada.
- Guías relacionadas: explora las guías de bidirectional y websockets para profundizar.
- Patrones complementarios: revisa los patrones de diseño aplicables a tu stack tecnológico.
- Postmortems públicos: estudia incidentes reales de equipos que enfrentaron problemas similares en producción.
Notas de Producción
- Despliega gradualmente usando canary o blue-green para detectar regresiones temprano.
- Configura alertas para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
- Documenta el rollback en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
- Revisa logs estructurados con correlation IDs para trazar requests end-to-end en incidentes.
Puntos Clave
- Aplica construye un chat bidireccional con websocket y node.js cuando necesites una solución práctica para tu caso de uso.
- Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
- Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
- Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.
Errores Comunes en Producción
- Copiar el ejemplo sin adaptarlo a volúmenes y modos de fallo reales.
- Saltar tests de carga e inyección de errores antes del primer despliegue productivo.
- Codificar valores fijos que deberían ser configurables por entorno.
- Olvidar agregar logging y monitoreo en cada paso.
- Desplegar sin plan de rollback ni estrategia de backup probada.
- Asumir que el ejemplo mínimo escalará sin agregar caché o procesamiento por lotes.
- No documentar la versión y configuración usadas en producción.
- Dejar la receta sin cambios cuando evolucionan las dependencias o la escala.
Preguntas frecuentes
¿Cómo manejo el orden de mensajes con WebSocket?
WebSocket no garantiza el orden de mensajes entre reconexiones. Asigna un número de secuencia a cada mensaje en el servidor. En el cliente, bufferiza los mensajes fuera de orden y entregalos en secuencia. Para orden estricto, usa una cola de mensajes del lado servidor (Redis Streams, Kafka) y acuse cada mensaje antes de enviar el siguiente.
¿Cómo testeo conexiones WebSocket en CI?
Usa el cliente de la librería ws en tests de Node.js. Conéctate al servidor, envía un mensaje y aserta la respuesta. Para load testing, usa Artillery o k6 con escenarios WebSocket. Mockea el servidor WebSocket en tests unitarios usando mock-socket para evitar iniciar un servidor real.
¿Cómo implemento detección de presencia (estado online/offline)?
Rastrea las conexiones de usuarios en un Map<userId, Set<ws>> en el servidor. Al conectar, agrega el socket y broadcastea un evento presence:online a las salas del usuario. Al desconectar (escucha ws.on('close')), remueve el socket y broadcastea presence:offline si no quedan sockets para ese usuario. Usa un intervalo heartbeat ping/pong (30 segundos) para detectar conexiones stale. Limpia sockets muertos en el handler del intervalo para prevenir memory leaks.
¿Cuál es el número máximo de conexiones WebSocket concurrentes?
Node.js puede manejar aproximadamente 10.000-25.000 conexiones WebSocket concurrentes por proceso, dependiendo de la frecuencia de mensajes y tamaño del payload. Para escalar más allá, usa un cluster de procesos Node.js detrás de un load balancer con sticky sessions. Usa Redis Pub/Sub para broadcastear mensajes entre procesos. Para deployments más grandes, considera gateways WebSocket dedicados como Centrifugo o servicios managed como AWS API Gateway WebSocket.
¿Cómo manejo backpressure cuando un cliente es lento?
Las conexiones WebSocket pueden bufferizar mensajes si el cliente lee más lento de lo que el servidor envía. Monitorea ws.bufferedAmount — cuando excede un umbral (ej., 1MB), pausa el envío y reanuda una vez que el cliente drena el buffer. Para chat basado en salas, descarta mensajes no críticos (como indicadores de typing) bajo backpressure y encola mensajes importantes (como mensajes de chat) para entrega posterior. Implementa un check de highWaterMark antes de cada broadcast para evitar memory exhaustion. Loggea eventos de backpressure para identificar clientes lentos que pueden necesitar ser desconectados o rate-limited.
Para deployments multi-server, usa un store compartido de backpressure (Redis) para que todas las instancias sepan qué clientes están saturados.
Desconecta clientes que permanezcan saturados más allá de un timeout (ej., 30 segundos) para proteger la estabilidad del servidor.
Recursos Relacionados
Autenticacion y Patrones de Seguridad para WebSockets
Como autenticar conexiones WebSocket, implementar validacion de tokens y manejar autorizacion para mensajeria en tiempo real en produccion
RecipeImplementa Server-Sent Events en Go para Actualizaciones
Construí un endpoint de Server-Sent Events en Go listo para producción con gestión de conexiones, heartbeats y manejo graceful de desconexiones de clientes.
RecipeComunicación WebSocket en Tiempo Real con Socket.io en
Construye aplicaciones WebSocket en tiempo real en Node.js con Socket.io
RecipeServer-Sent Events con Node.js y Express
Construí push de servidor a cliente con Server-Sent Events en Node.js y Express. Cubre conexiones, heartbeats, reconexión y broadcast seguro.
RecipeWebSockets para Comunicación en Tiempo Real
Construye comunicación bidireccional en tiempo real con WebSockets, manejando gestión de conexiones, reconexión y fallbacks.