Comunicación en Tiempo Real

Socket.IO

Socket.IO es un framework de eventos en tiempo real basado en WebSockets. Añade salas, confirmaciones (acknowledgements), reconexión automática y un fallback de polling, para que dediques tu tiempo a las funcionalidades en lugar de a la infraestructura de la conexión.

intermediate14 min readUpdated 16 sept 2026
server.ts
ts
// server.ts
import { Server } from "socket.io";

const io = new Server(httpServer, {
  cors: { origin: "https://app.example.com" },
});

io.use((socket, next) => {
  const user = verifyToken(socket.handshake.auth.token);
  if (!user) return next(new Error("unauthorized"));
  socket.data.user = user;
  next();
});

io.on("connection", (socket) => {
  socket.join(`user:${socket.data.user.id}`);

  socket.on("message:send", (payload, ack) => {
    io.to(payload.room).emit("message:new", payload);
    ack({ ok: true });
  });
});
Lanzado
2010
Ejecutado en
Node.js
Transporte
WebSocket con fallback de polling
Protocolo
Engine.IO
Escalado con
Redis adapter
Cliente
Navegador y Node.js

Por que importa

Lo que Socket.IO te ofrece desde el primer día

Salas y namespaces

Agrupa sockets en el servidor y emite a una sala, un namespace o a todos excepto al emisor. Sin gestión manual de conexiones y con escalabilidad entre instancias.

Reconexión integrada

El cliente se reconecta con un sistema de backoff tras una caída de conexión y puede recuperar paquetes perdidos, evitando que las redes móviles o la suspensión del portátil rompan la app.

Eventos con confirmaciones

Cada emit puede llevar un callback, haciendo que la solicitud/respuesta sobre un socket se sienta como una llamada a una función en lugar de un juego de adivinanzas.

La imagen completa

Las tres capas de Socket.IO

Una capa de eventos para tu aplicación, una capa de transporte que comienza como HTTP polling y una capa de adaptador que escala entre instancias.

Eventos

Emitir

Todo es un evento nombrado. El servidor y el cliente emiten y escuchan en los mismos nombres de canal, y los payloads son JSON simples.

Transporte

Actualizar

La conexión comienza como HTTP long polling y se actualiza a WebSocket, permitiendo que sobreviva a proxies, navegadores antiguos y redes hostiles.

Adaptador

Escalar

Un adaptador difunde eventos entre instancias del servidor, lo que permite que múltiples pods sirvan una única aplicación lógica en tiempo real.

HTML5 de un vistazo

Las piezas que utilizarás

Eventos

socket.emit y socket.on mueven payloads nombrados en ambas direcciones.

Salas

socket.join(room) e io.to(room).emit(...) se dirigen a un subconjunto.

Namespaces

Divide una conexión en canales aislados como /chat y /admin.

Reconexión

Reconexión automática con backoff y recuperación de estado opcional.

Confirmaciones

Un callback confirma que el servidor recibió y procesó un evento.

Redis adapter

Comparte difusiones entre instancias y mantiene sesiones persistentes (sticky sessions).

Flujo

Un mensaje en tiempo real

Desde el handshake hasta la desconexión, cada interacción de Socket.IO sigue el mismo camino corto.

  1. 1

    Conectar

    El cliente abre una conexión y presenta sus credenciales en el payload de auth del handshake.

  2. 2

    Unirse a salas

    El servidor asigna el socket a salas basadas en el usuario autenticado, el tenant y cualquier suscripción.

  3. 3

    Emitir

    Un cliente emite un evento nombrado con un payload JSON y, opcionalmente, un callback para la respuesta.

  4. 4

    Difundir

    El servidor procesa el evento y emite a la sala correspondiente, para que cada socket interesado lo reciba.

  5. 5

    Desconectar

    Cuando la conexión se cierra, el socket abandona todas sus salas automáticamente y el servidor realiza la limpieza.

La guia completa

Socket.IO: Todo lo que necesitas saber

Qué añade Socket.IO a los WebSockets

Socket.IO es una librería para la comunicación en tiempo real basada en eventos entre un servidor y sus clientes. Internamente, utiliza el protocolo WebSocket cuando es posible y recurre a HTTP long polling cuando no puede hacerlo. Sobre ese transporte, te proporciona un pequeño protocolo de aplicación: eventos nombrados, rooms, acknowledgements, reconexión automática y heartbeats.

Esa capa adicional es precisamente el objetivo. Una conexión WebSocket pura te ofrece un canal y nada más. Cualquier aplicación real tiene que volver a construir lo mismo: ¿cómo agrupo las conexiones por sala de chat o tenant?, ¿cómo sé que un mensaje fue recibido?, ¿cómo reconecto de forma limpia?, ¿cómo hago un broadcast a través de varios servidores? Socket.IO responde a esas preguntas una sola vez, en una librería bien probada, para que puedas dedicar tu tiempo al producto.

La contrapartida es que Socket.IO no es un WebSocket puro. Define su propio handshake y formato de paquetes sobre Engine.IO, por lo que un cliente WebSocket nativo no puede conectarse a un servidor de Socket.IO. Ambas partes deben utilizar el cliente de Socket.IO. Si necesitas interoperabilidad con clientes WebSocket arbitrarios, utiliza la guía de WebSockets y la librería ws en su lugar.

El resto de esta guía asume que ya conoces el protocolo puro y que ahora buscas la versión “batteries-included”.

Configuración del servidor y del cliente

El servidor se acopla a un servidor HTTP existente, que generalmente es el mismo que sirve tu API. Esto significa un solo puerto, un único certificado TLS y sin infraestructura adicional.

import { Server } from "socket.io";
import { createServer } from "node:http";

const httpServer = createServer(app);

const io = new Server(httpServer, {
  cors: { origin: process.env.APP_ORIGIN, credentials: true },
});

io.on("connection", (socket) => {
  console.log("connected", socket.id);
});

httpServer.listen(3000);

El cliente se conecta con el mismo origen y se autentica a través del handshake.

import { io } from "socket.io-client";

const socket = io("https://api.example.com", {
  auth: { token: getAccessToken() },
  withCredentials: true,
});

socket.on("connect", () => console.log("connected", socket.id));
socket.on("disconnect", (reason) => console.log("closed", reason));

El socket.id es un identificador por conexión. Es útil para el logging y para dirigirse a una conexión específica, pero cambia al reconectar, por lo que nunca debe usarse como un id de usuario ni almacenarse como un estado persistente.

Eventos, acknowledgements y callbacks

Todo en Socket.IO es un evento nombrado que transporta un payload JSON. Tanto el servidor como el cliente utilizan emit para enviar y on para escuchar. Los nombres son simplemente strings, así que elige una convención y mantenla; noun:verb como message:send, message:new y presence:joined se lee bien en ambos lados.

La característica que distingue a Socket.IO de un socket básico es el acknowledgement. Si el emisor pasa un callback como último argumento, el receptor puede llamarlo para responder, convirtiendo el emit en una solicitud/respuesta sobre la misma conexión.

// client
socket.emit("message:send", { room: "general", body: "hello" }, (ack) => {
  if (!ack.ok) showError(ack.error);
});

// server
socket.on("message:send", (payload, ack) => {
  if (!payload.body) return ack({ ok: false, error: "empty" });
  io.to(payload.room).emit("message:new", payload);
  ack({ ok: true, at: Date.now() });
});

Utiliza acknowledgements para cualquier evento cuyo resultado el cliente necesite conocer: crear un registro, unirse a una sala o enviar un formulario. Para broadcasts puros donde nadie espera una respuesta, un emit simple es suficiente. Un punto medio es socket.timeout(5000).emit(...), que falla el callback si no llega un acknowledgement a tiempo.

Middleware y el pipeline de eventos

Socket.IO tiene dos capas de middleware, y colocar la lógica en la capa correcta permite mantener los handlers limpios.

El middleware de conexión, registrado con io.use o namespace.use, se ejecuta una vez por conexión antes del evento connection. Aquí es donde deben ir la autenticación, la resolución de tenants y la configuración por conexión. El middleware se ejecuta en el orden de registro, y cada uno llama a next() para continuar o a next(new Error(...)) para rechazar la conexión.

io.use((socket, next) => {
  const startedAt = Date.now();

  socket.on("disconnect", () => {
    metrics.observe("socket.duration", Date.now() - startedAt);
  });

  next();
});

El middleware de eventos, registrado con socket.use, se ejecuta para cada evento entrante en ese socket. Es el lugar natural para la validación, el rate limiting y el logging estructurado, ya que puede ver el nombre del evento y el payload antes que cualquier handler.

const chat = io.of("/chat");

chat.use((socket, next) => {
  if (!socket.data.user) return next(new Error("unauthorized"));
  next();
});

chat.use((socket, next) => {
  socket.onAny((event, ...args) => {
    logger.info({ event, userId: socket.data.user.id, args });
  });
  next();
});

socket.onAny observa cada evento entrante, y socket.onAnyOutgoing observa todo lo que envías; juntos, te proporcionan un trazo completo de una conexión sin tocar un solo handler. El middleware también reconoce los namespaces: el namespace /admin puede exigir un rol diferente sin afectar a /chat.

Rooms y namespaces

Existen dos mecanismos de agrupación para mantener los mensajes dirigidos en lugar de transmitirlos a todo el mundo.

Un room es una etiqueta del lado del servidor aplicada a un conjunto de sockets. Cualquier socket puede unirse o salir de cualquier room en cualquier momento, y un socket puede estar en varios rooms a la vez. Cuando emites un evento a un room, solo sus miembros lo reciben.

io.on("connection", (socket) => {
  socket.join(`user:${socket.data.user.id}`);

  socket.on("room:join", (room, ack) => {
    socket.join(room);
    ack({ ok: true });
  });
});

Un namespace es un canal de comunicación independiente bajo una ruta, como /chat o /admin. Los namespaces tienen su propio middleware, sus propios manejadores de conexión y sus propios rooms. Utiliza los namespaces para separar responsabilidades que no deban compartir eventos en absoluto —por ejemplo, un namespace de chat público y un namespace de administración interna— en lugar de usarlos para modelar datos dentro de una misma funcionalidad.

Los rooms son la herramienta principal. Modélalos basándote en las cosas que interesan a tus usuarios: una conversación, un documento, un tenant, un dashboard. De este modo, el broadcasting se convierte en una sola línea de código en lugar de un bucle sobre las conexiones.

Difusión y segmentación (Broadcasting and targeting)

Socket.IO tiene un vocabulario reducido para definir quién recibe un evento; configurarlo correctamente evita tanto las fugas de datos como el desperdicio de recursos en el envío masivo (fan-out).

  • io.emit(...) — todos los sockets conectados. Rara vez es lo que buscas.
  • socket.emit(...) — solo el socket que está manejando el evento actual.
  • socket.broadcast.emit(...) — todos excepto el remitente.
  • socket.to(room).emit(...) — todos en la sala excepto el remitente.
  • io.to(room).emit(...) — todos en la sala, incluyendo al remitente.
  • io.to(roomA).to(roomB).emit(...) — la unión de ambas salas.
  • socket.to(socketId).emit(...) — un socket específico mediante su id.
socket.on("typing", ({ room }) => {
  // Tell the room, but not the person typing.
  socket.to(room).emit("typing", { user: socket.data.user.id });
});

Encadenar to une a los destinatarios; no existe un operador de intersección en la API principal. Si necesitas “miembros de la sala A que también sean administradores”, modélalo como una sala independiente — room:${id}:admins — en lugar de intentar calcularlo al momento de emitir el evento.

Autenticando el handshake

La autenticación debe formar parte del handshake de la conexión, no del primer mensaje. El middleware de Socket.IO registrado con io.use se ejecuta antes del evento connection, por lo que puedes rechazar un socket no autenticado antes de que pueda emitir cualquier cosa o unirse a alguna sala.

io.use((socket, next) => {
  const token = socket.handshake.auth.token;

  try {
    const payload = verifyToken(token);
    socket.data.user = { id: payload.sub, roles: payload.roles };
    next();
  } catch {
    next(new Error("unauthorized"));
  }
});

Hay dos detalles importantes. Primero, socket.data es el lugar adecuado para adjuntar el estado por conexión; este persiste durante toda la vida de la conexión y está disponible en cada handler. Segundo, una conexión rechazada envía un evento connect_error en el cliente, permitiendo que la UI solicite un token nuevo en lugar de reintentar indefinidamente.

Para clientes de navegador, una cookie enviada con el handshake es una alternativa al token explícito, lo que permite reutilizar la infraestructura de sesión existente. En cualquier caso, establece siempre el origin de cors hacia tu propia aplicación y autoriza cada evento basándote en el usuario en socket.data: la autenticación demuestra quién se conectó, no qué tiene permitido hacer.

Reconexión y recuperación del estado de la conexión

Las conexiones se caen. Las laptops entran en modo suspensión, los teléfonos cambian de red, los balanceadores de carga se reinician. Socket.IO se reconecta automáticamente utilizando exponential backoff y jitter, y emite los eventos reconnect_attempt y reconnect para que puedas mostrar la interfaz de usuario adecuada.

Sin embargo, por defecto, los eventos enviados mientras el cliente estuvo ausente se pierden. El cliente se reconecta como un nuevo socket y reanuda la actividad desde ese momento. Esto es aceptable para un feed en vivo, pero incorrecto para un chat o un documento colaborativo.

La recuperación del estado de la conexión (connection state recovery) resuelve el caso de desconexiones breves. Actívala en el servidor y cualquier cliente que se reconecte presentando su ID de sesión recibirá los paquetes que perdió, siempre y cuando la desconexión haya sido corta.

const io = new Server(httpServer, {
  connectionStateRecovery: {
    maxDisconnectionDuration: 2 * 60 * 1000,
    skipMiddlewares: true,
  },
});

La recuperación está limitada deliberadamente: mantiene los paquetes recientes en memoria durante un intervalo corto y solo en la instancia que gestionó la conexión original, por lo que no es un sustituto del almacenamiento persistente. Cualquier dato que deba sobrevivir a una caída prolongada —mensajes, pedidos, ediciones de documentos— debe guardarse en una base de datos, utilizando el socket únicamente para notificar a los clientes que algo ha cambiado.

Escalado con el adaptador de Redis

Un servidor de Socket.IO mantiene sus sockets y salas en memoria. Si ejecutas dos instancias detrás de un balanceador de carga, tendrás efectivamente dos aplicaciones en tiempo real separadas: un mensaje emitido en la instancia A nunca llegará a los clientes de la instancia B.

El adaptador de Redis soluciona esto. Cada instancia publica sus broadcasts en un canal de pub/sub de Redis y se suscribe al mismo canal, de modo que un emit en una instancia se entrega a los sockets correspondientes en todas partes.

import { createAdapter } from "@socket.io/redis-adapter";
import { createClient } from "redis";

const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();

await Promise.all([pubClient.connect(), subClient.connect()]);

const io = new Server(httpServer, {
  adapter: createAdapter(pubClient, subClient),
});

Dos notas operativas. Primero, las sticky sessions siguen siendo necesarias durante la fase de HTTP polling, ya que el handshake de Engine.IO abarca varias solicitudes que deben llegar a la misma instancia. Configura el balanceador de carga para usar afinidad basada en cookies, o fuerza únicamente el transporte WebSocket. Segundo, el adaptador utiliza Redis pub/sub, que es rápido pero no duradero: un mensaje publicado mientras una instancia se está reiniciando se perderá. Redis también se trata en su propia guía de Redis.

Para despliegues muy grandes, existe un adaptador sharded que distribuye los canales a través de un clúster de Redis, así como adaptadores para otros brokers, pero comienza con el estándar.

Emitir desde fuera de un socket

Los eventos en tiempo real rara vez se originan dentro de un manejador de conexión. Una solicitud HTTP crea un comentario, un worker finaliza un reporte, un webhook confirma un pago; todos estos casos necesitan notificar a los clientes conectados.

Mantén una referencia a io y emite a una room desde cualquier lugar:

app.post("/comments", async (req, res) => {
  const comment = await db.comment.create({ data: req.body });

  io.to(`post:${comment.postId}`).emit("comment:new", comment);

  res.status(201).json(comment);
});

Las rooms son la herramienta adecuada en este caso porque se comparten entre instancias cuando el adapter está configurado. Para llegar a un usuario específico, emite a una room por usuario — user:${id} — a la que el socket se haya unido al momento de la conexión, en lugar de rastrear los socket ids. Esto permite que siga funcionando cuando el usuario tiene dos pestañas abiertas, se reconecta o cae en una instancia diferente.

Si realmente necesitas un socket id, io.in(room).fetchSockets() devuelve los sockets activos en una room, lo cual es útil para conteos de presencia y envíos dirigidos.

Presencia, indicadores de escritura y funciones colaborativas

Las salas junto con un store compartido cubren la mayoría de las funciones colaborativas. La presencia —quién está conectado— es la más común, y la versión ingenua deja de funcionar en el momento en que ejecutas más de una instancia.

El patrón consiste en mantener la presencia autoritativa en Redis, no en un map de JavaScript, y realizar la limpieza al desconectarse:

io.on("connection", async (socket) => {
  const { id } = socket.data.user;
  await redis.sadd("online", id);

  socket.on("disconnect", async () => {
    const sockets = await io.in(`user:${id}`).fetchSockets();
    if (sockets.length === 0) await redis.srem("online", id);
  });
});

La comprobación de fetchSockets es importante: un usuario con dos pestañas abiertas no debería aparecer como desconectado cuando se cierra una de ellas. Los indicadores de escritura, las posiciones del cursor y las banderas de “alguien está editando” siguen la misma estructura: estado efímero transmitido a una sala, con una expiración corta para que un cliente que haya fallado no deje un indicador obsoleto para siempre.

Para una edición colaborativa real con resolución de conflictos, recurre a una librería de CRDT como Yjs y envía sus actualizaciones a través del socket, en lugar de intentar inventar un algoritmo de fusión.

Validación, rate limiting y seguridad

Un socket es un canal de entrada no confiable, exactamente igual que una solicitud HTTP. Trata cada payload de evento como hostil hasta que sea validado.

  • Valida las estructuras. Analiza los payloads con una librería de esquemas como Zod antes de manipularlos, y recházalos con un acuse de recibo en lugar de lanzar una excepción.
  • Aplica rate limiting. Un socket puede emitir miles de eventos por segundo. Utiliza un token bucket por socket y desconecta o limita a los usuarios que abusen del sistema.
  • Limita el tamaño del payload. maxHttpBufferSize define el límite de cuánta información puede transportar un solo mensaje.
  • Autoriza cada evento. Vuelve a verificar que el usuario en socket.data tenga permiso para actuar sobre la sala o el recurso, y no te limites a comprobar que esté conectado.
  • Valida el origen. Configura cors.origin y no lo dejes abierto en producción.
  • Mantén los timeouts. Los heartbeats y los idle timeouts evitan que las conexiones abandonadas generen fugas de memoria.
io.on("connection", (socket) => {
  socket.use(([event, payload], next) => {
    const parsed = MessageSchema.safeParse(payload);
    if (!parsed.success) return next(new Error("invalid_payload"));
    if (!takeToken(socket.id)) return next(new Error("rate_limited"));
    next();
  });
});

El middleware por socket registrado con socket.use es el lugar ideal para centralizar estas comprobaciones, permitiendo que los handlers individuales se centren únicamente en la lógica de negocio.

Depuración y observabilidad

La primera herramienta ya viene integrada. Configurar DEBUG=socket.io:* (o engine*) imprime el handshake, las actualizaciones de transporte y el flujo de paquetes, lo cual suele ser suficiente para diagnosticar un cliente que no logra conectarse.

Para producción, monitorea las señales que realmente predicen incidentes:

  • Sockets conectados — una caída repentina indica un despliegue, un crash o una partición de red.
  • Eventos por segundo, por nombre — un pico en un evento suele ser el resultado de un bucle infinito en el cliente.
  • Motivos de desconexiónping timeout y transport close apuntan a problemas de red o del proxy, mientras que io server disconnect significa que tu código cerró el socket.
  • Estado del adapter — si el pub/sub de Redis está lento o desconectado, las emisiones entre instancias se detienen sin que parezca que ningún socket ha fallado.
io.on("connection", (socket) => {
  socket.on("disconnect", (reason) => {
    metrics.increment("socket.disconnect", { reason });
  });
});

El paquete @socket.io/admin-ui añade un dashboard sobre estos mismos datos, y vale la pena ejecutarlo en staging para poder ver las salas y los sockets a medida que cambian. Como ocurre con cualquier sistema en tiempo real, los fallos más confusos son los parciales: una instancia funciona correctamente, otra no, y solo una vista por instancia permite revelarlo.

Probando un servidor en tiempo real

El código en tiempo real es testeable. Inicia el servidor en un puerto efímero, conecta algunas instancias de socket.io-client y realiza aserciones sobre los eventos que reciben. La disciplina fundamental es esperar (await) los eventos en lugar de usar sleep, para que los tests sean rápidos y deterministas.

import { io as Client } from "socket.io-client";

test("broadcasts a message to the room", async () => {
  const a = Client(url, { auth: { token } });
  const b = Client(url, { auth: { token } });

  await Promise.all([once(a, "connect"), once(b, "connect")]);

  a.emit("room:join", "r1");
  b.emit("room:join", "r1");

  const received = once(b, "message:new");
  a.emit("message:send", { room: "r1", body: "hi" });

  const [message] = await received;
  expect(message.body).toBe("hi");

  a.close();
  b.close();
});

Prueba también los caminos de fallo, ya que es ahí donde residen los bugs de tiempo real: un cliente no autenticado debería recibir connect_error, un payload incorrecto debería ser rechazado mediante un acuse de recibo (acknowledgement), y un socket que se desconecta debería abandonar sus salas. Utiliza una sala o namespace único por test para que las pruebas paralelas no vean los eventos de las demás, y cierra siempre los clientes para que el proceso del test finalice correctamente.

Desplegando un servidor de Socket.IO

Un despliegue de Socket.IO es básicamente un despliegue HTTP con dos requisitos adicionales: conexiones de larga duración y estado compartido. La mayoría de los imprevistos surgen al olvidar uno de ellos.

  • Un solo puerto. Vincula Socket.IO al mismo servidor HTTP que tu API y termina el TLS en el proxy. No hay un puerto separado que exponer.
  • Soporte de proxy para upgrades. Nginx y la mayoría de los balanceadores de carga necesitan una configuración explícita para pasar los encabezados Upgrade y Connection; de lo contrario, la conexión se mantendrá silenciosamente en modo polling.
  • Sesiones persistentes (Sticky sessions). La afinidad basada en cookies mantiene el handshake de polling en una sola instancia. Si fuerzas el transporte exclusivo de WebSocket, la afinidad es menos crítica, pero el handshake aún debe completarse en algún lugar.
  • Adaptador de Redis. Configúralo antes de que exista la segunda instancia, no después de que los usuarios reporten mensajes perdidos.
  • Apagado gradual (Graceful shutdown). En SIGTERM, deja de aceptar conexiones y cierra el servidor para que los eventos en curso puedan finalizar.
process.on("SIGTERM", async () => {
  io.close(); // disconnects clients and stops the server
  await pubClient.quit();
  await subClient.quit();
  httpServer.close();
});
upstream io_nodes {
  ip_hash;
  server 10.0.0.1:3000;
  server 10.0.0.2:3000;
}

Dimensiona cada instancia según las conexiones que soporte, no solo por el volumen de peticiones. Cada socket consume memoria y un descriptor de archivo, y una sola instancia con decenas de miles de conexiones fallará de maneras que un servicio basado en peticiones nunca haría. Escala horizontalmente pronto, monitorea el recuento de conexiones y otorga a los despliegues un periodo de gracia lo suficientemente largo para que los clientes se reconecten a una instancia saludable.

Cuándo los WebSockets puros son la mejor opción

Socket.IO no siempre es la respuesta. Elige el protocolo puro cuando:

  • La interoperabilidad sea importante. Los clientes de WebSocket nativos, otros lenguajes y las herramientas estrictas de protocolo no pueden comunicarse mediante el handshake personalizado de Socket.IO.
  • Necesites el cliente más ligero posible. Socket.IO requiere un bundle del cliente; un WebSocket puro ya viene integrado en el navegador.
  • Tu infraestructura sea nativa de WebSocket. Algunos gateways, brokers y edge runtimes terminan conexiones de WebSockets pero no el fallback de polling de Socket.IO.
  • Controles ambos extremos y no quieras abstracciones. Si las salas y la reconexión son triviales para tu caso de uso, ws es más sencillo de gestionar.

Por el contrario, elige Socket.IO cuando quieras salas, acknowledgements, reconexión automática y broadcasting multi-instancia sin tener que construirlos desde cero. Para la mayoría de los equipos de producto, esa lista representa todo el conjunto de funcionalidades de su capa de tiempo real, que es precisamente la razón por la cual existe la librería.

Mejores prácticas

  • Autentica en io.use y almacena el usuario en socket.data, no en un closure.
  • Modela las rooms basándote en objetos reales del dominio: conversaciones, documentos, tenants.
  • Usa acknowledgements para eventos cuyo resultado necesite el cliente.
  • Prefiere socket.to(room) cuando el remitente no deba recibir su propio evento.
  • Habilita la recuperación del estado de la conexión, pero mantén el estado persistente en una base de datos.
  • Añade el adaptador de Redis antes de agregar una segunda instancia y configura sticky sessions.
  • Emite desde handlers de HTTP y workers a través de rooms, no mediante socket ids almacenados.
  • Valida, autoriza y aplica rate limit a cada evento.
  • Limpia la presencia en disconnect y usa fetchSockets para gestionar múltiples pestañas.
  • Configura maxHttpBufferSize, los orígenes de CORS y los timeouts de forma explícita.

Errores comunes

  • Llamar a io.emit cuando solo una room debería recibir el evento.
  • Asumir que Socket.IO y WebSockets son intercambiables y luego fallar al conectar un cliente nativo.
  • Confiar en socket.handshake.auth sin verificar el token.
  • Almacenar la presencia en un Map local y perderla al usar un load balancer.
  • Olvidar las sticky sessions y romper el handshake de polling.
  • Esperar que la reconexión repita los eventos sin una recuperación del estado de la conexión.
  • Usar socket.id como id de usuario y provocar errores al reconectar.
  • Realizar tareas pesadas o bloqueantes dentro de un event handler, ralentizando todos los sockets de la instancia.
  • Dejar la validación del payload y el rate limiting delegados al frontend.
  • Tratar el socket como almacenamiento duradero para cualquier dato que no deba perderse.

Próximos pasos

Socket.IO es la capa de alto nivel sobre el protocolo tratado en la guía de WebSockets, donde se explican los frames, los heartbeats y el handshake de actualización que ahora estás abstrayendo. La guía de Redis profundiza en el pub/sub y el estado compartido detrás del adapter, y Node.js Basics explica el event loop en el que se ejecuta cada handler. Si tu servidor de Socket.IO está vinculado a una API HTTP, la guía de Express cubre el routing y el middleware que comparten el mismo proceso.

En la practica

Servidor, cliente, salas y escalado

Los cuatro archivos detrás de la mayoría de las funcionalidades en tiempo real.

server.ts
import { Server } from "socket.io";
import { createServer } from "node:http";

const httpServer = createServer(app);

const io = new Server(httpServer, {
  cors: {
    origin: process.env.APP_ORIGIN,
    credentials: true,
  },
});

io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  try {
    socket.data.user = verifyToken(token);
    next();
  } catch {
    next(new Error("unauthorized"));
  }
});

io.on("connection", (socket) => {
  socket.join(`user:${socket.data.user.id}`);
  socket.emit("ready", { id: socket.id });
});

httpServer.listen(3000);

Salas de Socket.IO vs canales de WebSocket puros

Socket.IO rastrea la membresía por ti. Con el protocolo puro, debes mantener un mapa de conexiones y distribuir los mensajes manualmente, lo cual es propenso a errores sutiles.

Socket.IO
socket.join(`org:${orgId}`);
io.to(`org:${orgId}`).emit("update", payload);
WebSocket puro
// You own membership, fan-out and cleanup.
const rooms = new Map<string, Set<WebSocket>>();
for (const ws of rooms.get(`org:${orgId}`) ?? []) {
  if (ws.readyState === ws.OPEN) ws.send(payload);
}

Confirmación vs emit de 'disparar y olvidar'

Un callback convierte un emit en una solicitud/respuesta y expone los fallos. El 'disparar y olvidar' está bien para difusiones puras, pero no para acciones que puedan ser rechazadas.

Con ack
socket.emit("order:create", order, (result) => {
  if (!result.ok) showError(result.error);
  else markCreated(result.id);
});
Disparar y olvidar
socket.emit("order:create", order);
// No idea whether the server accepted,
// rejected or crashed.

Compromisos

¿Es Socket.IO la abstracción correcta?

Socket.IO intercambia una pequeña capa de protocolo y la necesidad de un cliente específico por una gran cantidad de infraestructura en tiempo real que, de otro modo, tendrías que escribir tú mismo.

Strengths

  • Funcionalidades, no infraestructura

    Salas, confirmaciones, reconexión, heartbeats y fallback de polling están incluidos. Son semanas de casos borde que no tendrás que descubrir en producción.

  • Una sola API en ambos lados

    El servidor y el cliente comparten el mismo modelo de eventos, por lo que una funcionalidad suele requerir unas pocas líneas en cada lado. Integrar a un desarrollador frontend toma minutos.

  • Escalabilidad entre instancias

    El Redis adapter convierte una flota de pods en un único servidor lógico para salas y difusiones, que es la parte más difícil de cualquier despliegue en tiempo real.

Trade-offs

  • No es WebSocket puro

    Socket.IO define su propio protocolo sobre Engine.IO. Un cliente WebSocket nativo no puede conectarse, por lo que necesitas el cliente de Socket.IO en cada plataforma.

  • Las salas viven en memoria

    La membresía de las salas se mantiene por proceso a menos que uses un adaptador. Sin Redis, una reconexión en otra instancia puede aterrizar silenciosamente en el lugar equivocado.

  • Fácil emitir en exceso

    io.emit envía a cada socket conectado. Está a una pulsación de tecla y puede filtrar datos o colapsar un despliegue grande, así que dirígete a las salas deliberadamente.

Preguntas frecuentes

Preguntas frecuentes

Keep learning

Related topics from the roadmap.

$ comienza a aprender

Listo para aprender Socket.IO?

Nuestro tutorial interactivo te guia a traves de Socket.IO paso a paso — con quizzes y codigo real que puedes ejecutar en el navegador.