v-taiwan #3: del socket storm al Realtime Owner — el frontend también es costo de sistema
Charla de Fortes Huang en el v-taiwan Meetup #3: cómo domar el “socket storm” cuando un usuario abre varias pestañas de una app de trading en tiempo real. La premisa que lo cambia todo:
El frontend no es solo UI — es parte del costo de sistema y de la gobernanza del flujo de datos.
El problema en números
Cada pestaña abierta es un runtime completo: instancia de Vue, store de Pinia, observers de TanStack Query, cliente de Socket.IO, gráfico de velas, watchers. Con tres pestañas, cada tick de mercado se procesa por triplicado.
El ejercicio de capacity thinking (estimación de magnitud, no benchmark): si el 70% de usuarios abre 1 página, el 25% abre 2 y el 5% abre 5, el promedio es 1,45 páginas/usuario. Con 100.000 usuarios online son 145.000 runtimes — y si limitas a 1 conexión realtime por usuario, evitas 45.000 streams (~31%). Detrás de cada stream evitado no hay solo una conexión: hay JSON parse, invalidación de computed/watch, validación de schema, callbacks del chart y presión de GC que dejan de ejecutarse.
Realtime Owner: quién tiene derecho al tick
La estrategia elegida: una sola pestaña (Leader/Owner) conecta al socket /quote y consume el tick realtime; las demás (Followers) solo ven history por REST y pueden reclamar el rol cuando lo necesitan. La coordinación viaja por BroadcastChannel — pero solo mensajes de control:
- Sí es su responsabilidad: leader election (heartbeat, release, stale takeover), request/release de ownership, señales de invalidación tras un CRUD
- No es su responsabilidad: transportar el tick de mercado — el Follower no recibe ni renderiza el tick; solo el Leader lo consume
Esta separación es literalmente data plane / control plane: el canal de alta frecuencia (Binance → backend → leader tab) nunca se mezcla con el de baja frecuencia (coordinación).
Un matiz honesto del speaker sobre la versión “Policy-Based” con múltiples owners: es la más completa en el diagrama, pero “chachi para ver, no para operar” — leader election con edge cases, heartbeat, debugging de “por qué esta pestaña no es leader”. Empezar simple con maxRealtimeOwners=1 y añadir policy solo si el producto lo pide. La tensión clásica entre arquitectura de pizarra y lo que realmente shippeas.
CRUD sin refetch storm
Cuando un admin muta datos, callUpdate viaja como señal — no es el dato, es solo “este recurso cambió”. La invalidación local es inmediata, pero el refetch de red lleva jitter para no generar picos de QPS: en el ejemplo del grid de 9 pantallas con el mismo par, 1 mutación de admin no debe disparar 9 refetchs — el leader hace 1 (con dedupe + jitter) y distribuye el snapshot a los followers.
Por qué el límite vive en el backend
La parte que más me gustó, porque es puro pensamiento de backend: BroadcastChannel solo coordina pestañas del mismo origen en el mismo navegador — no ve otros dispositivos del usuario. Solo el backend tiene la vista global por userId/sessionId, y además el límite depende del plan (móvil 1, desktop 3-5, trader profesional 10): es una regla de negocio, no de UI. El análogo directo en mi mundo es maximumSessions de Spring Security — el servidor decide cuántas sesiones concurrentes permite, no el navegador.
Otros dos paralelismos con el stack Java que anoté:
- Precision boundary: el backend (Rust) pasa los precios como
Binance string → BigDecimal (DB) → string (API)— nuncaf64/numberhasta que el frontend decide cómo mostrar. Exactamente la regla deBigDecimalpara dinero en Java. committed_broadcast_failed: distinguir “nunca pasó” de “pasó pero nadie se enteró”. Si el commit ya ocurrió y falla el broadcast, no se reintenta el write — solo se resincroniza. Es el problema que resuelve el patrón outbox en sistemas con Kafka + DB transaccional.
Y sobre validación en el cliente: en Spring Boot, @Valid + Jackson protegen la frontera HTTP gratis; en un socket del frontend no hay contenedor — lo que llega es literalmente any. Zod es ese borde: valida la forma en runtime, falla rápido y claro (si el backend cambia un campo de string a number lo ves en el primer tick, no como un NaN silencioso tres componentes después), y con z.infer el tipo TS se genera desde el schema — una sola fuente de verdad.
Repos del demo: frontend (Vue 3 + TS) y backend (Rust + PostgreSQL).