El balance: cuándo compensa la conexión directa
Comparadas en dinero, en código y en fiabilidad, las conexiones directas ganan en pocos escenarios concretos y pierden frente a un relevo simple en casi todo lo demás.
Las cuatro lecciones anteriores han desmontado la pila pieza a pieza y todas apuntaban a la misma pregunta, que ahora toca responder sin rodeos: dado todo lo que cuesta, ¿cuándo merece la pena una conexión directa entre dos clientes y cuándo es preferible un servidor que se limite a reenviar? La respuesta no es ideológica y no depende de ninguna preferencia sobre arquitecturas distribuidas. Depende de tres monedas que se pueden contar por separado —dinero, código y fiabilidad— y de un cuarto factor que decide la mayoría de los casos reales y que casi nunca aparece en la discusión, porque no tiene que ver con la red sino con el tiempo: dos dispositivos que nunca están encendidos a la vez no pueden hablarse directamente por muy bien que atraviesen sus routers. Esta lección pone números a las tres monedas, desarrolla ese cuarto factor y termina proponiendo la única disposición de piezas que, a estas alturas del recorrido, resiste el examen.
- Comparar en dinero el coste de relevar tráfico frente al de reenviarlo todo por servidor.
- Medir el coste en código y en superficie de fallo de cada una de las dos opciones.
- Entender por qué la sincronización asíncrona obliga a tener una réplica siempre disponible.
- Identificar los escenarios concretos en que la conexión directa sí gana con claridad.
- Diseñar una capa de sincronización indiferente al transporte que permita usar ambos.
Las tres monedas, contadas
Empecemos por el dinero, que es el argumento que más se invoca y el que peor se calcula. La intuición dice que si los datos van directos no pasan por tu servidor y por tanto no pagas ancho de banda. La intuición olvida dos cosas: que una fracción de las conexiones acabará en tu relevo de todos modos, y que las cargas de sincronización local-first son diminutas comparadas con lo que la pila fue diseñada para transportar.
// Comparacion honesta para una carga tipica de sincronizacion
const usuarios = 5_000;
const opsPorUsuarioDia = 400; // ediciones, no fotogramas de video
const bytesPorOp = 300; // una actualizacion convergente comprimida
const gbMesTodoPorServidor =
(usuarios * opsPorUsuarioDia * bytesPorOp * 30 * 2) / 1024 ** 3; // entra y sale
const fraccionRelevada = 0.15; // lo que no logra camino directo
const gbMesSoloRelevo = gbMesTodoPorServidor * fraccionRelevada;
// El ahorro existe, pero se mide en unidades que no cambian ninguna decision.
Ese cálculo produce, para una aplicación de tamaño respetable, cifras de tráfico que caben holgadamente en el plan más barato de cualquier proveedor. El ahorro que aporta la conexión directa es real y es irrelevante. La conclusión cambia por completo cuando el dato deja de ser una operación y pasa a ser un fichero: bibliotecas de fotos, proyectos de vídeo, conjuntos de datos científicos o copias de seguridad mueven órdenes de magnitud más y ahí el argumento económico se invierte de golpe.
La segunda moneda es el código, y aquí la asimetría es brutal y no se corrige con experiencia. La opción indirecta requiere un servidor que acepte canales, los mantenga abiertos y reenvíe, más un bucle de reconexión en el cliente. La opción directa requiere todo eso —porque la señalización es exactamente ese mismo servidor— y además la gestión del inventario de candidatas, la emisión de credenciales efímeras para el relevo, la operación del propio relevo, el manejo del reinicio cuando cambia la red, la administración de una conexión por cada pareja de participantes, la contrapresión de cada canal, el troceado de mensajes y, obligatoriamente, el camino alternativo para quien no consiga conectar. Es decir: la opción directa no sustituye a la indirecta, se suma a ella.
La tercera moneda es la fiabilidad, y su comparación es la más incómoda. Un reenvío por servidor funciona siempre que el servidor esté en marcha, y esa es una propiedad que controlas, que mides y que puedes mejorar pagando. Una conexión directa funciona para una fracción de los intentos que no controlas en absoluto, porque depende de routers ajenos, de políticas de operador y de cortafuegos corporativos sobre los que no tienes ninguna influencia y de los que ni siquiera obtienes un diagnóstico útil cuando fallan.
Hay un límite estructural que conviene tener presente antes de dibujar cualquier arquitectura de pares. En una malla donde todos hablan con todos, el número de conexiones crece con el cuadrado del número de participantes, y cada una de esas conexiones lleva su propio inventario de candidatas, su propio saludo criptográfico, su propio transporte con estado y su propio tráfico de mantenimiento periódico. Con cuatro o cinco participantes es perfectamente manejable. Con quince, cada cliente sostiene más de un centenar de conexiones sumando las de todos, gasta batería en mantenerlas vivas y envía cada actualización tantas veces como pares tenga. La salida habitual es elegir un participante como concentrador, lo cual reintroduce un punto central con la desventaja añadida de que ese punto es el portátil de alguien.
El factor que decide casi todos los casos: el tiempo
Aquí está el argumento que suele faltar en las comparativas y que, cuando aparece, resuelve la discusión. Una conexión directa exige que los dos extremos estén encendidos, conectados y ejecutando la aplicación en el mismo instante. Es un requisito trivial en una videollamada, porque una videollamada es por definición un encuentro simultáneo. Es un requisito devastador en sincronización local-first, porque el patrón de uso dominante es exactamente el contrario: edito en el portátil, lo cierro, cojo el metro, abro el móvil. Los dos dispositivos son míos, tienen los dos la misma cuenta, y no han coincidido despiertos ni un segundo.
sequenceDiagram participant P as Portatil participant S as Replica siempre disponible participant M as Movil P->>S: cambios locales al cerrar Note over P: apagado Note over M: se enciende mas tarde M->>S: que hay de nuevo S->>M: los cambios del portatil Note over P,M: nunca coincidieron despiertos Note over S: sin esta pieza la sincronizacion no ocurre
De ahí se sigue una conclusión que atraviesa todo el resto del recorrido: un sistema local-first necesita al menos una réplica que esté disponible cuando los demás no lo están. Puede ser un servidor propio, un servicio de almacenamiento, un ordenador de la casa que no se apaga o cualquier otra cosa, pero tiene que existir, y su papel no es mandar sobre los datos sino simplemente estar. En cuanto se acepta que esa pieza es obligatoria, la pregunta cambia de forma: ya no es si tener servidor o no tenerlo, sino qué hacer con el que forzosamente vas a tener.
Y ahí el análisis se vuelve casi aritmético. Esa réplica siempre disponible ya mantiene canales abiertos, ya sabe quién es quién, ya guarda y ya reenvía. Añadirle el papel de señalización cuesta muy poco. Añadirle el papel de transportar directamente las actualizaciones, para cargas ligeras, cuesta prácticamente lo mismo. La conexión directa, en ese contexto, deja de ser un cambio de arquitectura y pasa a ser lo que realmente es: una ruta alternativa más rápida y más barata para el subconjunto de casos en que los dos extremos coinciden despiertos y sus redes lo permiten.
Dónde la conexión directa sí gana con claridad
Nada de lo anterior significa que la conexión directa no valga la pena nunca. Significa que su ámbito es más estrecho y más específico de lo que sugiere el entusiasmo, y conviene nombrarlo con precisión porque en esos casos la diferencia es enorme.
Misma red y datos grandes
Dos equipos en la misma oficina moviendo gigabytes por la red local a una velocidad que ningún servidor remoto puede igualar y con coste cero.
Efímero y sensible a latencia
Cursores, presencia, voz y vídeo, que es aquello para lo que la pila se diseñó y donde cada ida y vuelta de más se nota en la piel.
Sin conexión al exterior
Trabajo de campo, aulas, barcos, obras y cualquier sitio con red local pero sin salida, donde el servidor sencillamente no está.
Volumen que sí pesa en la factura
Cargas de vídeo o de ficheros grandes a gran escala, donde el ancho de banda del servidor sí es una partida presupuestaria seria.
Hay un quinto argumento que se invoca a menudo y que merece un examen más frío: la privacidad. Es cierto que en una conexión directa el servidor no ve los datos, pero también lo es que con cifrado de extremo a extremo sobre un reenvío tampoco los ve, y esa segunda opción es muchísimo más barata de construir y funciona el cien por cien de las veces. Lo que la conexión directa sí protege y el reenvío cifrado no es el metadato: quién habla con quién, cuándo y cuánto. Para la inmensa mayoría de las aplicaciones eso no justifica el coste; para algunas concretas, es la razón de existir. La distinción importa porque se decide con criterios distintos.
Escribe la capa de sincronización sin que sepa por dónde viaja lo que envía. Su interfaz debe ser enviar un conjunto de actualizaciones a un par identificado y recibir otro tanto, sin ninguna referencia al transporte. Con eso, el reenvío por servidor y la conexión directa son dos implementaciones intercambiables de la misma interfaz, se pueden tener ambas activas a la vez, y añadir o quitar la segunda deja de ser una reescritura para convertirse en un cambio de configuración. Las bibliotecas maduras de este espacio están organizadas exactamente así, con proveedores de transporte enchufables, y no es casualidad ni elegancia: es la manera de no quedarse atrapado en una apuesta que se tomó antes de tener datos.
La disposición que resiste el examen
Puesto todo junto, la arquitectura que sale no es un compromiso tibio sino una jerarquía con un orden justificado en cada escalón. Debajo, como suelo, la réplica local del dispositivo, que es lo que hace que la aplicación funcione sin red y lo que define de verdad el término local-first. Encima, una réplica siempre disponible que recibe y guarda, que es lo que permite sincronizar entre dispositivos que nunca coinciden. Encima, cifrado de extremo a extremo, que es lo que hace que esa réplica no necesite ser de confianza. Y por último, opcionalmente y como optimización, el camino directo cuando ambos extremos están despiertos, están cerca y sus redes lo permiten.
// La capa de sincronizacion no sabe ni le importa por donde va
const sync = crearSincronizador({ almacen, identidad });
sync.anadirTransporte(relevoPorServidor({ url: "wss://sync.ejemplo.com" }));
// Y esto es aditivo: si falla, si no conecta o si se elimina, nada se rompe.
if (soportaConexionDirecta() && preferenciaUsuario.p2p) {
sync.anadirTransporte(conexionDirecta({ senalizacion: mismoServidor }));
}
Ese orden tiene una propiedad que conviene subrayar: cada escalón funciona sin el de arriba. La aplicación es útil solo con el primero. Sincroniza entre dispositivos con los dos primeros. Es privada con los tres. Y es rápida y barata con los cuatro. Un diseño así se puede entregar por partes, se puede degradar sin avisar al usuario y se puede revertir si el escalón superior resulta no compensar. La disposición inversa, que empieza por la conexión directa y añade el servidor como remiendo cuando falla, tiene las cuatro propiedades contrarias.
El error más caro de todo este espacio, y el que ha consumido más años de ingeniería en proyectos que no llegaron a nada, es una confusión de vocabulario que parece inocente: tratar local-first y punto a punto como si fuesen la misma idea. No lo son, y ni siquiera están en el mismo eje. Local-first es una afirmación sobre dónde reside la copia que manda y qué puede hacer el usuario sin permiso de nadie: si el dato está entero en tu dispositivo, si la aplicación responde sin latencia de red, si sigue funcionando en un avión, si puedes llevártelo cuando el servicio cierre. Punto a punto es una afirmación sobre la topología del transporte: por qué cable viajan los bytes entre dos máquinas. Se puede tener lo primero sin lo segundo con toda naturalidad, y de hecho es lo que hacen casi todas las aplicaciones local-first que funcionan y que la gente usa a diario: réplica completa en el dispositivo, convergencia demostrable, y un servidor tonto que recibe y reparte actualizaciones cifradas que no puede leer. Y se puede tener lo segundo sin lo primero, que es el caso de cualquier aplicación de comunicación en tiempo real que hable directamente entre navegadores y no guarde absolutamente nada al cerrar la pestaña. Confundirlas tiene un coste concreto y medible, y va siempre en la misma dirección: se gasta el presupuesto de complejidad en la topología, que es la mitad que no aporta ninguna de las propiedades que se prometieron al usuario, y se llega tarde y sin fuerzas a la mitad que sí las aporta. Todo lo que hace valiosa una aplicación local-first —la respuesta instantánea, el funcionamiento sin red, la propiedad real de los datos, la resistencia al cierre del servicio, la posibilidad de exportar y de auditar— sale de la réplica local, del modelo de datos que converge y del formato abierto en que se guarda. Absolutamente nada de eso sale de que el paquete evite un salto por un centro de datos. Por eso el orden correcto de trabajo, si hubiera que resumir el nivel en una sola instrucción, es este: construye primero todo lo que da valor sin red, hazlo funcionar contra el servidor más aburrido que puedas escribir, cífralo para que ese servidor no importe, y solo entonces, con usuarios reales y con mediciones sobre la mesa, pregúntate si la ruta directa resuelve un problema que alguien esté teniendo. En la mayoría de los productos la respuesta será que no, y ese no es un fracaso: es haber ahorrado un año. En unos pocos será que sí, y entonces la ruta directa entrará como lo que siempre debió ser, una optimización enchufable sobre un sistema que ya funcionaba sin ella.
- Calcula el tráfico mensual de tu aplicación si todo pasara por servidor y ponlo al lado del precio real de ese ancho de banda.
- Estima qué porcentaje de tus usuarios comparte red con otro de sus propios dispositivos y con qué frecuencia.
- Registra durante una semana cuántas veces dos dispositivos de la misma persona están despiertos a la vez.
- Escribe la interfaz de tu capa de sincronización sin ninguna mención al transporte y comprueba que es implementable dos veces.
- Implementa primero el reenvío por servidor con cifrado de extremo a extremo y mide la satisfacción antes de añadir nada.
- Enumera qué propiedades prometidas a tus usuarios dependen de la ruta directa y comprueba si alguna sobrevive al examen.