Comunicación en Tiempo Real en Node.js
"Tiempo real" en una API de Node.js casi siempre significa una cosa específica: el servidor necesita decirle algo a un cliente conectado antes de que ese cliente lo solicite.
Busca en todas las páginas de la documentación
"Tiempo real" en una API de Node.js casi siempre significa una cosa específica: el servidor necesita decirle algo a un cliente conectado antes de que ese cliente lo solicite.
El HTTP simple nunca fue construido para eso; cada intercambio comienza con una solicitud del cliente, y un servidor solo puede responder a una solicitud que ya llegó.
WebSocket, Server-Sent Events y long-polling son tres formas diferentes de adaptar ese modelo de "primero la solicitud" a algo que admita envíos iniciados por el servidor, y cada uno hace un compromiso diferente entre direccionalidad, complejidad y compatibilidad de infraestructura.
Conceptos Básicos de Tiempo Real muestra el código para elegir entre ellos; esta página trata sobre el problema subyacente que todos resuelven y el costo que cada solución impone a un proceso de Node una vez que las conexiones comienzan a acumularse.
El modelo de solicitud-respuesta de HTTP significa que el servidor es fundamentalmente reactivo: solo puede hablar cuando se le habla, respondiendo a una solicitud y luego, en el modelo clásico, cerrando ese intercambio.
Eso está bien para la carga de una página, pero se rompe en el momento en que un servidor necesita decirle a un cliente ya conectado "algo cambió" sin que ese cliente lo haya preguntado en el último segundo.
Tres mecanismos resuelven esto, cada uno relajando una parte diferente de las reglas de HTTP:
data: en ella con el tiempo, que la API EventSource del navegador convierte de nuevo en eventos discretos.Una forma útil de visualizar la diferencia: WebSocket es como abrir una línea telefónica y dejarla conectada para que cualquiera de las partes pueda hablar cuando quiera; SSE es como una transmisión de radio unidireccional a la que el oyente se sintonizó, donde solo la estación transmite; long-polling es como llamar repetidamente a alguien y preguntar "¿hay algo nuevo?", esperando en espera hasta que tengan noticias o cuelguen.
// SSE: el servidor nunca "termina" esta respuesta, simplemente sigue escribiendo
res.writeHead(200, { "Content-Type": "text/event-stream" });
res.write(`data: ${JSON.stringify({ price: 142.5 })}\n\n`);
// la conexión permanece abierta; más llamadas a res.write() siguen más tardeQué mecanismo encaja depende casi por completo de la direccionalidad: ¿el cliente necesita enviar datos a través del mismo canal después de la conexión inicial, o solo recibe?
La diferencia mecánica más profunda entre estos tres no es el formato de cable, sino lo que cada uno le cuesta a un proceso de Node mantener abierto.
Una solicitud HTTP ordinaria ocupa recursos del servidor solo durante la breve ventana entre la llegada y la respuesta; una conexión persistente (WebSocket o SSE) ocupa un socket, un trozo de memoria para búferes y estado por conexión, y una entrada en la contabilidad del bucle de eventos mientras esté abierta, independientemente de si fluyen datos activamente o no.
Ese es un modelo de recursos fundamentalmente diferente: un servidor de solicitudes por segundo escala con la tasa de solicitudes, mientras que un servidor de conexiones persistentes escala con el número de conexiones mantenidas simultáneamente; un servicio con tráfico modesto pero 50,000 conexiones WebSocket abiertas está bajo una presión de memoria real incluso si los mensajes son raros.
El bucle de eventos de un solo hilo de Node maneja esto razonablemente bien para las conexiones inactivas, ya que un socket abierto pero silencioso cuesta memoria pero no CPU; el bucle solo trabaja cuando un mensaje llega realmente a uno de ellos.
Donde se vuelve costoso es en la distribución activa (fanout), es decir, transmitir un mensaje a muchas conexiones a la vez, porque serializar y escribir en cada socket es un trabajo síncrono que ocupa el mismo hilo único detrás del cual esperan los mensajes de todas las demás conexiones.
// La transmisión es un trabajo O(n) en el único hilo que comparten todas las conexiones.
// Esta es la parte que consume CPU, no las conexiones inactivas en sí mismas.
for (const client of connectedClients) {
client.send(payload); // cada llamada se ejecuta en serie en el mismo bucle de eventos
}Esa única línea es la razón por la que "cuántas conexiones puede mantener Node" y "qué tan rápido puede Node transmitir a todas ellas" son dos preguntas diferentes con dos respuestas diferentes: el recuento de conexiones inactivas está limitado principalmente por la memoria, pero el rendimiento de la transmisión está limitado por cuánto trabajo síncrono cuesta cada send(), multiplicado por la cantidad de destinatarios.
La autenticación también tiene su propia particularidad mecánica aquí: debido a que una conexión persistente no es una solicitud nueva cada vez, las credenciales deben verificarse una vez, en el momento de la conexión o actualización; un WebSocket no tiene un "encabezado de autorización" por mensaje como lo tiene HTTP, por lo que un socket no autenticado al que se le permite conectarse primero y verificarse más tarde es una brecha real y explotable.
Un solo proceso de Node no es donde los sistemas en tiempo real permanecen por mucho tiempo una vez que necesitan escalar más allá de una instancia, y ahí es donde aparece un segundo problema, más difícil: la distribución (fanout) entre procesos.
Si dos usuarios en el mismo canal de chat se conectan a dos instancias de Node diferentes detrás de un balanceador de carga, un mensaje de uno debe llegar de alguna manera al otro; las conexiones WebSocket en sí mismas son locales al proceso, por lo que la instancia A no tiene forma directa de escribir en un socket que la instancia B está manteniendo.
La solución estándar es un bus de mensajes compartido (Redis pub/sub es la opción común en esta pila) al que se suscribe cada instancia: la instancia A publica el mensaje una vez, y cada instancia, incluida la B, lo recibe y lo reenvía a cualquiera de sus propios sockets locales que les interese.
Esa única adición cambia el modo de falla de todo el sistema: el recuento de conexiones ya no necesita un enrutamiento fijo para "simplemente funcionar" por corrección, pero el bus de mensajes en sí mismo se convierte en una nueva dependencia que debe permanecer activa, y las garantías de ordenación/entrega de mensajes ahora son tan fuertes como las que proporciona el bus.
Escalado en Tiempo Real cubre este patrón (sesiones pegajosas, adaptadores de Redis y las compensaciones operativas) en profundidad; el punto a este nivel es reconocer por qué una solución de un solo proceso deja de ser suficiente mucho antes de que el recuento de conexiones sin procesar sea el cuello de botella.
Existen bibliotecas de nivel superior específicamente para absorber esta complejidad: Socket.IO agrupa la reconexión automática, la distribución basada en salas y un adaptador de Redis para este problema exacto entre instancias, a costa de un protocolo personalizado superpuesto a WebSocket (o un transporte de respaldo) en lugar de tramas WebSocket sin procesar.
| Enfoque | Fortaleza | Debilidad | Mejor Ajuste |
|---|---|---|---|
WebSocket puro (ws) | Control total, sobrecarga mínima, protocolo estándar | Tú mismo construyes la reconexión, las salas y la distribución | Protocolos personalizados, aplicaciones bidireccionales críticas en latencia |
| Server-Sent Events | HTTP simple - funciona a través de la mayoría de los proxies/balanceadores de carga sin modificar, reconexión automática del navegador | Solo de servidor a cliente; no hay canal de cliente a servidor en la misma conexión | Feeds en vivo, paneles, flujos de notificación |
| Long-polling | Funciona dondequiera que funcione HTTP simple, incluidos entornos de red hostiles | Mayor latencia, rotación constante de conexiones, más carga del servidor por mensaje | Solo ruta de respaldo, rara vez una primera opción hoy en día |
| Socket.IO | Reconexión, salas y distribución multi-instancia incorporados | Protocolo personalizado - no interoperable con clientes WebSocket puros; peso de dependencia añadido | Equipos que quieren el problema de escalado mayormente resuelto de fábrica |
La entrega iniciada por el servidor: llevar datos a un cliente ya conectado sin que ese cliente tenga que volver a solicitarlos. El HTTP simple solo admite lo contrario: un cliente que pregunta y un servidor que responde.
El modelo de solicitud-respuesta de HTTP requiere que exista una solicitud antes de que se pueda enviar una respuesta; no hay un canal para que el servidor escriba por iniciativa propia, a menos que algo (una actualización, una respuesta mantenida abierta o un sondeo repetido) cambie eso.
Comienza como una: una solicitud HTTP con un encabezado Upgrade, pero una vez que el servidor responde 101 Switching Protocols, ambas partes dejan de hablar HTTP por completo e intercambian mensajes enmarcados sin procesar sobre el mismo socket TCP mientras esté abierto.
SSE se define como parte de la API EventSource del navegador, que tiene un comportamiento de reconexión incorporado especificado como parte del estándar. WebSocket es un protocolo de nivel inferior sin tal contrato de API; la reconexión se deja completamente al código de la aplicación.
No por sí mismo; las conexiones inactivas consumen principalmente memoria (búferes de socket y estado por conexión), y el bucle de eventos no trabaja en una conexión hasta que un mensaje necesita ser enviado o recibido en ella.
Normalmente el volumen de mensajes, específicamente la distribución (fanout) de la transmisión: enviar a muchas conexiones a la vez es un trabajo síncrono en el único hilo del bucle de eventos, por lo que compite con los mensajes de todas las demás conexiones, mientras que las conexiones inactivas por sí solas solo cuestan memoria.
Cada conexión WebSocket es mantenida por el proceso específico que la aceptó; no hay una forma incorporada para que un proceso de Node escriba en un socket que otro proceso está manteniendo, por lo que los sistemas en tiempo real entre instancias necesitan un bus de mensajes compartido.
Rara vez para sistemas nuevos; principalmente como un respaldo cuando la infraestructura (ciertos proxies corporativos) bloquea las conexiones persistentes por completo. Cuesta más latencia y carga del servidor por mensaje que las alternativas.
No, es un protocolo personalizado superpuesto a WebSocket (con un transporte de respaldo para entornos que lo bloquean), por lo que un cliente WebSocket puro no puede comunicarse con un servidor Socket.IO sin la biblioteca cliente correspondiente.
En el momento de la conexión o actualización, antes de que se acepten los mensajes; una conexión persistente no tiene un equivalente por mensaje de un encabezado HTTP Authorization, por lo que verificar la autenticación "después del primer mensaje" deja una ventana real donde un socket no autenticado ya está conectado.
Solo una vez que ejecutes más de una instancia de servidor; una función en tiempo real de un solo proceso puede transmitir directamente a sus propias conexiones locales. Una capa pub/sub compartida solo se vuelve necesaria cuando un mensaje de una instancia necesita llegar a un cliente conectado a otra.
ws - detalles de implementación de WebSocket puroVersiones de la pila: Esta página fue escrita para Node.js 24 LTS, npm 10+ y TypeScript 5.6+.
Revisado por Chris St. John·Última actualización: 15 jul 2026