Capítulo 08 · 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.
18 min intermedio
¿Es tan importante?
Imaginate que seguimos en la misma tienda. Ya tenemos base de datos, load balancer, CDN, caché, colas, locks y rate limiting. Casi todos esos capítulos salen en una entrevista de system design aunque el enunciado no diga «tiempo real». WebSockets, no. Salen cuando el problema es tiempo real de verdad: chat, documento a la vez, un precio que late, un marcador, un juego. Si el prompt es «diseñá Dropbox» o «diseñá un acortador», sacar WebSockets es ruido.
Cuando el enunciado 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 en la pizarra. 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, y conviene no mezclarlos. «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 infraestructura de conexiones largas para un ticker. En una entrevista técnica, eso es yellow flag.
El número que no puede esperar al refresh
Alguien tiene abierta la ficha del 123. En la esquina pone «Quedan 5». En otra pestaña, el pedido 456 reserva un par con el lock o la transacción de stock. 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é, pero peor: disparás a propósito.
Un millón de fichas abiertas, cada una cada cinco segundos, son 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 otra vez.
Antes de saltar a WebSocket hay atajos. Long polling deja una GET abierta hasta que hay dato o timeout. SSE (Server-Sent Events) es una conexión HTTP que el servidor usa como manguera, solo hacia vos: el navegador reconecta solo, y si el cliente tiene que decir algo lo dice por un 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 es otro: 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 que llega y se va.
Dos hops, no un rayito
Cuando el stock pasa de 5 a 4, hay dos tramos. Mezclarlos es el error típico de la pizarra.
El hop 1 es cómo llega el 4 al móvil del visitante: 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.
Elegir WebSocket no escala nada por sí solo. Lo que escala es 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. Por eso hay 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 en memoria: es un TTL, algo como SET user:456 online EX 60. Cada pong refresca la key. Si deja de llegar, expira sola.
Las conexiones se caen siempre, así que el cliente reconecta. No en un bucle a cero milisegundos — eso es un DDoS a tu propio fleet —. Usás 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 la base de datos o en el log. Al reconectar, el cliente pide 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 — la misma idea que el outbox, vista 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 (caro y absurdo) o no persistís nada (peligroso).
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 de conexiones 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: un connection registry. Algo como user_id → servidor. Redis vale — el mismo Redis que en caché hacía de L2, otra vez otro job y otra estructura. Si el usuario tiene móvil y portátil, el valor es un set de servidores. 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 de oficio. 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 es 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 hace replay.
El camino cuando el «te la aparto» no se puede perder es otro: persistir en la base de datos, publicar en Kafka (o SQS, o Rabbit), y que la capa de WebSocket consuma ese log. Redis queda para presence, registry y fan-out en vivo. Broker más base de datos para lo que tiene que existir mañana. Es la misma lección que el pedido 456: at-least-once, idempotencia, y 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: un solo evento, muchos destinos. Con mil pestañas, Redis Pub/Sub suele alcanzar. Con diez millones de followers de un influencer que publica «nuevo drop», no. Si el productor intenta un for y empujar a cada uno dentro del mismo request, se queda trabado. El patrón correcto es otro: el evento entra a una cola, workers lo procesan en tandas, y recién al final cada WebSocket o SSE entrega el mensaje. El socket es el último centímetro. El trabajo pesado es repartir.
Para organizar esos destinos usás una room: un nombre de grupo. product:123 es la room de la ficha. El chat del pedido 456 es otra room, más chica. No es magia de protocolo: es «esta conexión está suscrita a este canal».
Ahí aparece un matiz que en entrevista conviene nombrar. Authentication dice quién sos: el JWT en el handshake. Authorization dice qué podés hacer: entrar a private-order-456 se comprueba al join, no solo al conectar. Si no, cualquiera con un token válido se cuela en el chat de Ana.
El orden de los mensajes también depende de la room. En el chat del 456, si ves «¿talla 42?» después de «te la aparto», la conversación miente. Por eso los mensajes llevan un sequence por conversación, no un contador global de toda la tienda. Es la misma idea que particionar por order_id en colas: orden local, no orden planetario. En cambio, en «12 personas mirando esta ficha», el orden casi no importa: da igual si el 11 llega un milisegundo antes que el 12.
Y queda el caso del móvil lento. El productor puede tirar diez mil mensajes por segundo; el 3G del comprador aguanta mil. Si empujás todo igual, la cola del cliente crece y la app se pone mala. Eso se llama backpressure: frenar o descartar cuando el receptor no da abasto. En la práctica, tirás lo descartable (typing...), te quedás solo con el último stock y no con los cincuenta valores intermedios (coalesce: muchos updates, un solo «quedan 4»), y si el cliente va irremediablemente atrás lo desconectás para que vuelva a pedir el estado con after=seq. No es ser cruel: es no mentirle con un stream atrasado.
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 la base de datos. Presence se reconstruye con heartbeats.
Redis se cae. Presence puede degradar y el hop 2 de Pub/Sub se queda ciego. El chat durable no debería depender solo de Redis; por eso existe el broker.
El visitante pierde el «quedan 4» un minuto: da igual. El comprador pierde el «te la aparto»: no da igual. Al reconectar, hace 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
→ load balancer
→ servidores WS / SSE
→ Redis: registry, presence, pub/sub
→ base de datos: mensajes y pedidos
→ broker → de vuelta a WS / SSE
Worker inventario → broker
Si lo contás en voz alta, suena así: 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 duro 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 del hop 2. 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.