WebSocket en Node.js: servidor, cliente, heartbeat y reconexión
WebSocket en Node.js: servidor ws, cliente nativo, heartbeat | We.Inc
Crea un servidor WebSocket en Node.js con el paquete ws, conéctate con el cliente WebSocket nativo de Node, detecta sockets muertos con ping y pong y reconecta con backoff.
Node.js no tiene un servidor WebSocket integrado, así que la mayoría de proyectos instalan el paquete ws y crean un WebSocketServer. Para el lado del cliente, Node incluye un WebSocket global compatible con el del navegador, añadido en v21.0.0 y v20.10.0, sin flag desde v22.0.0 y marcado como estable en v22.4.0. Añade un heartbeat con ping y pong y la reconexión con backoff antes de pasar a producción.
Qué pieza hace qué
Tarea
Usa
De dónde viene
Aceptar conexiones
new WebSocketServer({ port })
Paquete ws (npm install ws)
Conectar desde Node
new WebSocket(url)
Global en Node 22 y posteriores
Conectar desde un navegador
new WebSocket(url)
Integrado en todos los navegadores modernos
Encontrar sockets muertos
ws.ping() más un listener de pong
Paquete ws
Sobrevivir a reinicios
Reconectar con backoff exponencial
Tu propio código de cliente
Esta guía construye un pequeño servidor tipo chat y los clientes que hablan con él, en orden: un servidor que hace eco y difunde mensajes, un script de Node que se conecta sin ninguna dependencia, un heartbeat que descarta las conexiones que murieron en silencio y un cliente que vuelve después de que el servidor se reinicie. La calculadora de abajo muestra cómo crecen tus tiempos de reintento antes de ponerlos en código.
Ejecuta npm install ws en un proyecto cuyo package.json tenga "type": "module", para que funcione la sintaxis import de los ejemplos.
Arranca el archivo del servidor con node server.js. Escucha en el puerto 8080 y registra cada mensaje.
Ejecuta el archivo del cliente en una segunda terminal con Node 22 o posterior, o abre la consola del navegador y pega el mismo código.
Detén el servidor con Ctrl+C y vuelve a arrancarlo para ver cómo el cliente que reconecta espera, reintenta y se recupera.
Añade siempre un listener de error a cada socket del lado del servidor. ws emite eventos error, y uno sin gestionar puede tumbar el proceso.
Comprueba req.headers.origin en el handler de conexión y cierra los sockets de orígenes a los que no das servicio; los navegadores lo envían, incluidas las páginas de atacantes.
Los proxies inversos cortan los sockets inactivos. nginx cierra un WebSocket con proxy si el backend no envía nada durante 60 segundos por defecto, así que mantén tu intervalo de ping por debajo de eso.
Añade jitter aleatorio a los tiempos de reconexión. Sin él, todos los clientes reintentan en el mismo instante tras un reinicio y golpean el servidor a la vez.
Los bucles de difusión deben saltarse los clientes cuyo readyState no sea OPEN, porque enviar a un socket que se está cerrando no sirve de nada.
Preguntas frecuentes
¿Node.js tiene WebSocket integrado?
Solo el cliente. La clase global WebSocket apareció en v21.0.0 y v20.10.0 tras el flag --experimental-websocket, perdió el flag en v22.0.0 y dejó de ser experimental en v22.4.0. Sigue sin haber un servidor integrado, por eso ws sigue siendo la opción habitual.
¿Uso ws o Socket.IO?
ws habla el protocolo WebSocket puro, así que cualquier cliente estándar puede conectarse, incluidos el navegador y el global de Node. Socket.IO añade su propio protocolo encima, con salas y reconexión automática, pero necesita su propia librería de cliente en el otro extremo.
¿Por qué mueren las conexiones sin un evento close?
Si una ruta de red desaparece, por ejemplo al cerrar la tapa de un portátil, nunca se envía un frame de cierre y el servidor se queda con un socket medio abierto. El heartbeat los encuentra: un socket que no respondió al último ping se termina en la siguiente ronda.
¿Puede un cliente de navegador enviar pings?
No. La API del navegador solo tiene send() y close(). Los navegadores sí responden por su cuenta con pongs a los pings del servidor, por eso el heartbeat del lado del servidor también funciona con clientes de navegador.
¿Cómo comparto un puerto con un servidor HTTP?
Crea tú mismo el servidor HTTP, por ejemplo con Express o node:http, y pásalo como new WebSocketServer({ server }). Las peticiones de upgrade van a ws y las peticiones normales a tu aplicación.
Cómo empieza una conexión
Un WebSocket nace como un GET de HTTP con Upgrade: websocket. El servidor ws lo comprueba, responde 101 Switching Protocols y dispara su evento connection con el nuevo socket y la petición original. Todo lo que quieras del handshake, como un token en la query string o la cabecera Origin, se lee de esa petición.
Los heartbeats, en palabras sencillas
El protocolo dice que un extremo que recibe un ping debe responder con un pong que lleve los mismos datos. El servidor marca cada socket como vivo cuando llega un pong y, con un temporizador, borra la marca y vuelve a enviar un ping. Cualquier socket que siga sin marca en el siguiente ciclo nunca respondió, así que el servidor lo termina.
El README de ws usa un temporizador de 30 segundos para esto. Elige un valor bastante por debajo del timeout de inactividad de cualquier proxy que tengas delante.
Reconectar sin machacar el servidor
Un buen cliente espera un poco tras la primera caída, luego más tras cada fallo, hasta un tope, y reinicia el contador cuando se abre una conexión. Repartir las esperas de forma aleatoria evita que una multitud de clientes vuelva toda a la vez.
Paso 1: un servidor con ws
server.js: eco y difusión
Cada mensaje se reenvía a todos los clientes abiertos. El listener de error evita que un socket defectuoso tumbe el proceso.
Paso 2: un cliente sin dependencias
client.js: el WebSocket global de Node
Funciona sin cambios en Node 22 o posterior y en la consola de un navegador, porque ambos exponen la misma clase WebSocket.
Paso 3: descarta conexiones muertas con ping y pong
Nuestra versión del patrón del README de ws: marca como vivo al recibir un pong y termina todo lo que se saltó la última ronda.
Paso 4: un cliente que reconecta
reconnect.js: backoff exponencial con jitter
La espera se duplica tras cada intento fallido hasta un tope, recibe una variación aleatoria y se reinicia cuando se abre una conexión.
Detrás de nginx
nginx solo deja pasar el upgrade cuando reenvías tú mismo las cabeceras Upgrade y Connection. Antes de nginx 1.29.7 también necesitas proxy_http_version 1.1.
location de nginx para el socket
proxy_read_timeout sube el límite de inactividad de 60 segundos; de todos modos, el heartbeat de arriba mantiene el tráfico en marcha.
Calculadora de espera de reconexión
Define una primera espera, un multiplicador y un tope para ver cuánto espera un cliente antes de cada reintento, y la espera más corta y más larga al añadir jitter.
Primera espera (ms)
Multiplicador
Tope (ms)
Intentos a mostrar
Añadir jitter (esperar entre la mitad y la espera completa)