Capítulo 06 · En vivo
Tiempo real
El stock baja mientras alguien mira la ficha. SSE vs WebSocket, hop 1 y hop 2, y cuándo no hace falta un rayo.
15 min intermedio
¿Es tan importante?
Caché y colas salen casi siempre. WebSockets no. Salen cuando el enunciado es tiempo real de verdad: chat, documento a la vez, un precio que late, un marcador, un juego. Si el problema es «diseñá Dropbox» o «diseñá un acortador», sacar WebSockets es ruido.
Cuando el prompt dice WhatsApp, Google Docs, un dashboard en vivo o «notificaciones al instante», este capítulo te sostiene. El senior no se nota por dibujar un rayo. Se nota por decir no hace falta WebSocket cuando basta con que el servidor empuje de vez en cuando.
En la tienda hay los dos casos. «Quedan 4 pares» en la ficha del 123 es un número que el servidor quiere empujar. El cliente no tiene que hablar por el mismo tubo. Eso puede ser SSE. El chat con el vendedor — «¿sirve la talla 42?» / «sí, te la aparto» — es ida y vuelta, frecuente, baja latencia. Ahí sí es WebSocket. Si juntas las dos cosas en un solo protocolo «porque es real-time», estás pagando infra de conexiones largas para un ticker. Yellow flag.
El número que no puede esperar al refresh
Alguien tiene abierta la ficha. En la esquina pone «Quedan 5». En otra pestaña, el pedido 456 reserva un par. Ahora quedan 4. Si el visitante tiene que pulsar F5, diseñaste una web de 2005. Si cada pestaña pregunta cada dos segundos GET /products/123/stock, volviste al problema del caché, peor: disparás a propósito.
Un millón de fichas abiertas, cada una cada cinco segundos: doscientas mil requests por segundo aunque el stock no se mueva. Pagás el coste de preguntar, no el de que ocurra algo. Eso es polling. Sirve para un prototipo o para un dato que cambia dos veces al día. No sirve para Black Friday.
HTTP clásico es un turno: preguntás, te responden, se acabó. El tiempo real empieza cuando el servidor avisa ahora, sin que hayas preguntado.
Atajos antes de WebSocket. Long polling: una GET abierta hasta que hay dato o timeout. SSE (Server-Sent Events): una conexión HTTP que el servidor usa como manguera, solo hacia vos. El navegador reconecta solo. El cliente, si tiene que decir algo, lo dice por POST aparte. Para el stock del 123, SSE es a menudo la respuesta correcta. WebSocket entra cuando los dos tienen que hablar por el mismo canal, mucho y ya: chat, cursores, un juego, un book de órdenes.
Un WebSocket nace como HTTP y se queda abierto. Upgrade: websocket, 101 Switching Protocols. A partir de ahí los dos envían frames cuando quieren. El coste por frame es pequeño. El coste de verdad: esa conexión vive en un proceso, ocupa memoria y un file descriptor. Un millón de pestañas es un millón de inquilinos. No es un request.
Dos hops, no un rayito
Cuando el stock pasa de 5 a 4, hay dos tramos. Mezclarlos es el error típico.
El hop 1 es cómo llega el 4 al móvil: polling, SSE o WebSocket. El hop 2 es cómo llega el evento al proceso que tiene esa conexión abierta. El 456 se reservó en un worker de inventario, quizá en otro datacenter. El visitante está colgado de un servidor de conexiones en Madrid. Esos dos no se conocen.
Pedido 456 reserva stock
→ hop 2: Pub/Sub o broker
→ proceso que tiene la pestaña
→ hop 1: SSE o WebSocket
→ visitante en la ficha
Elegir WebSocket no escala nada. Escala el hop 2: saber dónde está cada conexión y empujar el evento hasta ahí.
La conexión se muere y vos no te enterás
El visitante entra al metro. El WiFi se cae. Tu servidor sigue teniendo un socket que cree vivo. A un millón de conexiones, los muertos te comen la máquina. Ping/pong cada treinta segundos. Si no hay respuesta, cerrás. Eso es un heartbeat. No es cortesía. Es higiene.
La misma higiene alimenta el «en línea». Presence no es un flag eterno. Es un TTL: SET user:456 online EX 60. Cada pong refresca. Si deja de llegar, expira.
Las conexiones se caen siempre. El cliente reconecta. No en un bucle a cero milisegundos — eso es un DDoS a tu propio fleet. Backoff más jitter, igual que los retries de las colas.
La pregunta buena no es «¿reconecta?». Es qué pasa con los eventos durante el hueco. «12 personas mirando esto» se puede perder: al volver, pedís el número otra vez. El «¿te aparto la 42?» del vendedor no. Si el mensaje importa, no vive en el socket. Vive en Postgres o en el log. Al reconectar: GET /messages?after=seq. WebSocket es un canal de entrega. Un send que fue bien no significa que el mensaje exista mañana. Persistís antes de empujar. Lo mismo que outbox, visto desde el otro lado.
Typing, cursores, presence: efímeros. Chat, cobro, «tu pedido salió»: durables. Si mezclás los dos en el mismo tubo sin distinguirlos, o lo persistís todo o no persistís nada.
Un millón de pestañas no caben en un proceso
HTTP escala fácil: cada request es un desconocido. WebSocket no. El servidor tiene la conexión. En Black Friday abrís tres servidores WS detrás de un load balancer. El visitante de la ficha 123 cayó en A. El worker de inventario no sabe qué es A.
Hace falta un mapa: connection registry. user_id → servidor. Redis vale — el mismo Redis que en caché hacía de L2: otro job, otra estructura. Si el usuario tiene móvil y portátil, el valor es un set. Sin mapa y sin un bus entre nodos, el evento del stock se queda en el worker y la pestaña no se entera.
Sticky sessions intentan devolver al mismo cliente al mismo servidor. Simplifican si hay estado local. No son la estrategia de escala. Un deploy tira esas conexiones igual. Lo que escala es poco estado en el nodo y un hop 2 compartido.
El load balancer también cambia. WebSocket es TCP largo. Un LB de capa 4 suele romper menos el upgrade. Uno de capa 7 es más listo para HTTP y SSE. Conexión persistente bidireccional: pensá L4. Ficha y APIs normales: L7. No pongas un proxy HTTP que cierre a los 60 segundos el socket que acabás de abrir.
Cómo se entera el servidor B
El vendedor escribe «te la aparto». Su socket está en A. El comprador está en B. A no puede hablarle a B por arte de magia.
El camino rápido: Redis Pub/Sub. A publica en chat:order:456. B está suscrito y empuja al móvil. Baja latencia. Si B está caído en ese segundo, el mensaje del canal se evaporó. Pub/Sub no es la cola del capítulo anterior. No guarda, no replay.
El camino cuando el «te la aparto» no se puede perder: persistir en DB, publicar en Kafka (o SQS, o Rabbit), y que la capa WS consuma ese log. Redis para presence, registry y fan-out live. Broker más DB para lo que tiene que existir mañana. Misma lección que el 456: at-least-once, idempotencia, outbox si el chat se guarda y se publica a la vez.
Vendedor escribe
→ validar
→ persistir el mensaje
→ publicar el evento
→ el nodo que tiene al comprador empuja
Al revés — empujar y luego guardar — es cómo un send exitoso te deja sin rastro.
Un gol, diez millones de pantallas
«Quedan 4» lo tienen que ver las mil pestañas abiertas en el 123. Eso es fan-out. Mil es un problema de Pub/Sub. Diez millones de followers de un influencer que publica «nuevo drop» no es un for en el request. Es evento → cola → workers en tandas. El WebSocket es el último centímetro, no el motor.
Una room es un nombre para un grupo. product:123 es una room. El chat del pedido 456 es otra, más pequeña, y con permiso.
Eso es authorization, distinto de authentication. El JWT en el handshake dice quién sos. Poder estar en private-order-456 se comprueba al join, no solo al conectar.
El orden: en el chat del 456, «¿talla 42?» después de «te la aparto» miente. Los mensajes llevan sequence por conversación, no global. En «12 personas mirando», el orden da igual. Es la misma pregunta que las particiones por order_id.
Y el móvil lento. El productor a diez mil mensajes por segundo, el 3G a mil. Backpressure otra vez. Tirás el typing.... Te quedás el último stock, no los cincuenta intermedios (coalesce). Si el cliente va irremediablemente atrás, lo desconectás y que resincronice.
Cuando se rompe
El servidor A se estrella. El cliente reconecta, cae en B, el registry se actualiza. Los mensajes de chat no estaban solo en la RAM de A: estaban en DB. Presence se reconstruye con heartbeats.
Redis se cae. Presence puede degradar. El hop 2 de Pub/Sub se queda ciego. El chat durable no debería depender solo de Redis; por eso el broker.
El visitante pierde el «quedan 4» un minuto. Da igual. El comprador pierde el «te la aparto». No da igual: al reconectar, catch-up por secuencia.
El cobro del 456 no va por WebSocket. El socket puede avisar «pagado». El dinero vive en Stripe y en Postgres. Notificar no es transaccionar.
El 123 y el 456, en vivo
El visitante abre la ficha. Para el stock, SSE (o un WebSocket si ya lo tenés para otra cosa). Se suscribe a product:123. El 456 reserva un par. El worker de inventario publica. Hop 2: Redis Pub/Sub o el broker. El proceso que tiene esa pestaña empuja «quedan 4».
El comprador abre el chat del pedido. Handshake con token. Join a order:456 con permiso. Escribe. Validar, persistir, publicar, empujar al vendedor que está en otro nodo. Sequence por conversación. Si el metro se lo traga, backoff, reconecta, after=seq. Heartbeat para el puntito verde.
Visitante / comprador
→ LB
→ servidores WS / SSE
→ Redis: registry, presence, pub/sub
→ DB mensajes y pedidos
→ broker → de vuelta a WS
Worker inventario → broker
Contalo: el stock es un empujón; el chat es un tubo de dos sentidos; lo importante no vive en el tubo; entre máquinas hay un bus; si hay millones de destinos, el trabajo es el fan-out.
Si el enunciado es un feed o un marcador, di SSE y quedate en el hop 2. Si es WhatsApp, di WebSocket y lo mismo. Si es Google Docs, el socket es el canal; el problema de verdad es el conflicto de escrituras, no el upgrade a 101.
Qué espera cada nivel
Un mid dibuja un WebSocket y un Redis Pub/Sub.
Un senior elige SSE cuando solo hay que empujar, nombra hop 1 vs hop 2, persiste antes de send, y no usa el socket como fuente de verdad.
Si te lo preguntan
¿Por qué no polling? Because we would pay for asking even when nothing happened. A persistent connection lets the server push.
¿SSE o WebSocket? SSE if the server only pushes — stock, scores, notifications. WebSocket if both sides talk often on the same channel — chat, collab, trading. Reaching for WebSockets the moment I hear real-time is a yellow flag.
¿Cómo escalas? Many connection servers behind a load balancer, little local state, a registry plus Redis or a broker for hop 2. Sticky sessions help; they are not the scaling plan.
¿Conexiones muertas? Ping/pong. No pong, we close and drop presence.
¿El cliente se desconecta? Backoff and jitter. Durable events are in the database; the client resyncs with after=seq. The socket is a delivery path, not the source of truth.
¿Redis Pub/Sub o Kafka? Pub/Sub for live ephemeral fan-out. Kafka when the event must survive a down consumer — same rule as the orders chapter.
Si podés contar la ficha del 123 y el chat del 456, y pararte a decir «esto no necesita WebSocket», este capítulo hizo su trabajo.