Gana la última escritura: la estrategia que destruye en silencio
La estrategia por defecto de medio sector converge sin discusión y cuesta casi nada, y a cambio destruye trabajo ajeno de forma sistemática y silenciosa porque quien decide no es el tiempo sino el reloj más adelantado.
Si abres al azar un motor de sincronización, una base de datos distribuida o un backend móvil, la probabilidad de que su política por defecto sea gana la última escritura es abrumadora. No es un descuido ni una moda: la estrategia tiene virtudes reales y verificables, y quien la eligió no era ingenuo. Converge siempre, no necesita historia, cuesta un campo por registro y se implementa en una tarde. El problema no está en ninguna de esas virtudes, sino en que todas ellas se cobran sobre una única propiedad que nadie discute cuando se elige y todo el mundo echa de menos cuando falla: la estrategia destruye trabajo real, de usuarios reales, sin dejar rastro y sin decírselo a nadie. Esta lección la desmonta pieza a pieza —cómo funciona, quién decide de verdad, qué se pierde exactamente— y termina delimitando el territorio, más pequeño de lo que parece, donde sí es la respuesta correcta.
- Reconstruir el mecanismo completo, incluido el desempate, y entender de dónde vienen sus virtudes.
- Ver por qué el árbitro efectivo no es el tiempo sino la deriva relativa de los relojes.
- Enumerar con precisión qué se destruye: la escritura perdedora, la señal de que existió y la posibilidad de recuperarla.
- Delimitar el conjunto de datos para los que la política es genuinamente adecuada.
El mecanismo y el origen de su atractivo
La estrategia cabe en una frase: cada escritura lleva adherida una marca de tiempo, y cuando dos escrituras compiten por el mismo dato sobrevive la de marca mayor. Como dos relojes distintos pueden producir el mismo valor, hace falta un desempate determinista —habitualmente el identificador de la réplica— para que todos los nodos elijan al mismo ganador. Con eso, la estructura resultante es un registro de último escritor, y satisface formalmente las propiedades que garantizan convergencia: la operación de fusión es conmutativa, asociativa e idempotente, de modo que las réplicas terminan idénticas independientemente del orden y del número de veces que intercambien.
// Un registro de ultimo escritor completo cabe en ocho lineas
function fusionar(a, b) {
if (a.ts !== b.ts) return a.ts > b.ts ? a : b;
return a.replica > b.replica ? a : b; // desempate determinista obligatorio
}
const local = { valor: "Informe trimestral", ts: 1772000000123, replica: "n7" };
const remoto = { valor: "Informe Q1 revisado", ts: 1772000000119, replica: "a2" };
fusionar(local, remoto); // -> el de n7, por cuatro milisegundos de reloj
Enumeradas juntas, sus ventajas explican perfectamente su dominio. El coste de metadatos es constante: un entero por dato, frente al vector proporcional al número de réplicas que exige la detección de concurrencia. No requiere conservar historia, de modo que no crece con el uso ni obliga a podar nada. Es indiferente al almacenamiento subyacente y encaja igual en una tabla relacional, en un documento o en un par clave-valor. Y sobre todo, no exige del programador ninguna decisión por tipo de dato: se aplica uniformemente a todo el modelo, lo cual permite entregar sincronización sin haber pensado en el modelo.
Esa última virtud es también la trampa, y conviene verla con claridad porque es donde se instala la política sin que nadie la haya elegido. Al no exigir decisiones, la estrategia se convierte en el valor por defecto; al ser el valor por defecto, se aplica a campos sobre los que nadie ha pensado; y como converge sin fallar, no genera ningún síntoma que obligue a pensar en ellos más tarde. Es una política que se instala sola y que no vuelve a pedir la palabra.
Conviene además notar dónde suele estar escrita. Rara vez aparece como una decisión explícita en el código de la aplicación: vive dentro de la librería de sincronización, del proveedor de datos móvil o del propio motor de la base distribuida, donde figura en la documentación como una nota técnica de dos líneas sobre resolución de conflictos. Quien integró esa pieza estaba resolviendo el problema de que los datos viajaran, no el de qué ocurre cuando se cruzan, y la política llegó de acompañante. Por eso la pregunta que abre cualquier auditoría honesta de un sistema local-first no es qué política elegiste, sino cuál heredaste sin saberlo y en qué línea de qué dependencia está escrita.
No conviene leer esta lección como una acusación contra quienes la diseñaron. La popularizaron almacenes distribuidos que resolvían un problema muy concreto: mantener disponibles millones de claves entre centros de datos con relojes razonablemente sincronizados y con escrituras que en su inmensa mayoría eran actualizaciones de estado de máquina, no ediciones humanas. En ese contexto —relojes controlados, tasa de concurrencia baja, valores sin residuo emocional— la política es una elección de ingeniería impecable. El fallo no fue suyo: fue trasplantarla, sin revisar ninguno de esos tres supuestos, a un mundo donde los relojes son de usuarios, la desconexión dura días y lo que se escribe es el trabajo de alguien.
Quien decide no es el tiempo, es el reloj más adelantado
Aquí empieza la parte incómoda, y no es la que la mayoría espera. La objeción habitual —los relojes no están perfectamente sincronizados— es correcta pero se queda corta, porque sugiere que el problema es de precisión y que se arreglaría con mejores relojes. El problema real es de sesgo persistente. La deriva de un reloj no es un ruido que se cancele con el tiempo: es un desplazamiento sistemático de un dispositivo concreto, que dura semanas y que convierte a ese dispositivo en ganador estructural de todas las disputas en las que participe.
Piensa en lo que eso significa en producción. Si el portátil de un usuario adelanta dos minutos respecto al servidor de tiempo, ese usuario gana absolutamente todos los conflictos con sus compañeros, incluidos aquellos en los que su edición fue anterior en el sentido causal y en el sentido humano. Y a la inversa: quien tenga el reloj atrasado no consigue que ninguna de sus escrituras concurrentes sobreviva jamás, sin que exista ningún síntoma que lo delate, porque el sistema no falla —funciona exactamente como fue diseñado—. La aplicación no está eligiendo la escritura más reciente: está eligiendo la del dispositivo peor calibrado.
flowchart TD P[portatil con reloj dos minutos adelantado] --> M[sus escrituras llevan marca alta] T[tableta con reloj correcto] --> B[sus escrituras llevan marca baja] M --> G[gana todas las disputas concurrentes] B --> X[pierde todas y nadie lo detecta] X --> S[el usuario ve su cambio desaparecer sin aviso] style P fill:#f9e2af,color:#11111b style G fill:#a6e3a1,color:#11111b style X fill:#f38ba8,color:#11111b style S fill:#f38ba8,color:#11111b
A esto se añade una familia de patologías que no son derivas sino saltos. Un reloj puede retroceder: al sincronizarse con la red tras haber ido adelantado, al cambiar el usuario la fecha del sistema, al arrancar un dispositivo cuya pila de reloj se ha agotado, al volver de una suspensión larga. Cuando eso ocurre, una escritura nueva nace con una marca inferior a la de una escritura vieja del mismo dispositivo, y queda permanentemente incapaz de sobrescribirla: el usuario edita, ve el cambio aplicado en local, y al sincronizar reaparece el valor antiguo una y otra vez sin explicación posible.
Es habitual responder a todo lo anterior con relojes lógicos híbridos, que combinan la marca física con un contador que garantiza monotonía y respeta la causalidad. La mejora es genuina y merece la pena: eliminan los retrocesos, acotan la ventaja de un reloj adelantado y aseguran que si una escritura sucedió después de otra, su marca es mayor. Pero conviene no confundir lo que arreglan con lo que no tocan. Un reloj híbrido produce un orden total y, como vimos en la primera lección, un orden total no puede representar el caso incomparable: dos escrituras genuinamente concurrentes seguirán recibiendo marcas distintas y una seguirá borrando a la otra. Has arreglado la injusticia arbitraria; la destrucción silenciosa sigue exactamente donde estaba.
Qué destruye exactamente, y por qué nadie se entera
Conviene ser preciso con el inventario de daños, porque la frase se pierde un cambio no transmite ni de lejos el alcance. Lo primero que desaparece es el valor perdedor completo, y si la granularidad es el registro entero, desaparece con él todo lo que ese registro contenía aunque el otro escritor no lo hubiera tocado: dos personas editan campos distintos del mismo cliente y el que llega con marca menor pierde también las modificaciones que nadie disputaba. Lo segundo que desaparece —y es peor— es la señal de que existió una alternativa. No hay excepción, no hay entrada en ningún registro, no hay marca en la interfaz. Y lo tercero es la posibilidad de recuperación, porque el valor descartado no se guarda en ningún sitio del que puedas sacarlo después.
Hay además un daño estructural que aparece en cuanto entran los borrados y que sorprende a mucha gente en producción. Si eliminar un dato consiste en quitarlo de la tabla, un borrado no tiene marca de tiempo con la que competir, y cualquier réplica que aún conserve el registro lo reintroducirá en la siguiente sincronización creyendo que el otro nodo simplemente no lo conocía. El elemento resucita. La corrección estándar es marcar la baja con una lápida que sí lleva marca y participa en la comparación, lo cual funciona a cambio de que esas lápidas deban conservarse indefinidamente o podarse con un criterio que, si se equivoca, vuelve a resucitar registros.
Resurrección de borrados
Sin lápidas con marca, un borrado no compite y cualquier réplica desactualizada reintroduce el dato. Con lápidas, arrastras metadatos para siempre.
Pérdida colateral por granularidad
Si la unidad es el registro, el perdedor se lleva por delante campos que nadie disputaba y que no participaron en el conflicto.
Silencio total
No hay error, ni aviso, ni registro. El fallo no aparece en la telemetría porque desde dentro del sistema nunca ocurrió nada anómalo.
El cambio fantasma
El usuario ve su edición aplicada en local, cierra el portátil convencido, y descubre días después que nunca llegó a existir para nadie más.
Y hay una variante del daño que solo aparece con colas de envío diferido, es decir, exactamente en el escenario para el que existe una aplicación local-first. Si una escritura se genera sin conexión, se guarda en una cola con su marca de origen y se envía tres días después, esa marca compite contra escrituras que ocurrieron mucho más tarde y pierde todas. El comportamiento es coherente con la política —la marca es antigua, luego pierde— y es exactamente lo contrario de lo que la persona esperaba, porque desde su punto de vista acaba de conectarse para que su trabajo llegue. Sellar en cambio la escritura en el momento del envío la haría ganar contra todo lo ocurrido durante esos tres días, incluidas escrituras que sí vieron el estado. Ninguna de las dos opciones es correcta, y esa ausencia de opción correcta es el diagnóstico: el problema no está en cuándo se sella, está en pretender ordenar totalmente lo que no tiene orden.
El silencio es la propiedad que hace que todo esto sea tan duradero. Un fallo ruidoso se corrige porque interrumpe a alguien; un fallo silencioso se acumula durante años porque su única manifestación es un goteo de incidencias de soporte que se cierran como no reproducible, ya que en efecto no hay nada que reproducir: el sistema hizo lo que debía. La aplicación no tiene manera de saber que perdió datos, así que su telemetría es impecable, sus pruebas pasan y su cuadro de mandos está en verde mientras el usuario reescribe por tercera vez el mismo párrafo.
// La misma operacion, con la senal conservada: el coste anadido es una linea
function fusionarConRegistro(a, b, contexto) {
const concurrentes = comparar(a.vv, b.vv) === "concurrentes";
const ganador = a.ts > b.ts || (a.ts === b.ts && a.replica > b.replica) ? a : b;
if (concurrentes) {
contexto.registrar({ campo: a.campo, ganador, descartado: ganador === a ? b : a });
}
return ganador;
}
Observa que ese fragmento necesita un vector de versiones que la política, por sí sola, no requería. Ahí está el coste real de dejar de perder en silencio y conviene enunciarlo sin disfraz: para poder contar que hubo un conflicto hay que detectarlo, y detectarlo cuesta los metadatos causales que la estrategia presumía de ahorrar. La elección honesta nunca fue entre una política barata y otra cara, sino entre un sistema que sabe lo que hace y otro que no puede saberlo; y el precio de saber es un vector por dato.
Merece la pena mirar bien ese fragmento, porque desmonta la justificación que más se oye. Conservar la información no obliga a abandonar la política: puedes seguir eligiendo por marca de tiempo y aun así registrar que hubo dos versiones y cuál se descartó. Lo que hace insostenible la estrategia tal como se despliega habitualmente no es su criterio de desempate, sino que se implementa sin detección, y por tanto sin la posibilidad de contar nada. La versión con registro es peor política y mejor sistema; la versión sin registro es la misma política y ningún sistema.
Dónde sí es la respuesta correcta
Sería deshonesto terminar sin delimitar el territorio legítimo, que existe y no es despreciable. La política es adecuada cuando el dato satisface tres condiciones a la vez. Primera: es una muestra de una realidad externa que evoluciona por su cuenta, de modo que el valor más reciente es genuinamente el más válido y no una opinión que compita con otra. Segunda: los valores anteriores no tienen valor residual, es decir, a nadie le sirve de nada recuperarlos. Y tercera: la escritura es atómica y completa, no una edición incremental sobre un estado que el escritor leyó antes.
La posición de un cursor, el estado de presencia de un usuario, la última ubicación conocida de un vehículo, la lectura de un sensor, el nivel de batería, la entrada de una caché derivada: todos cumplen las tres y para todos la estrategia no solo es aceptable sino óptima, porque cualquier maquinaria adicional gastaría recursos en conservar algo que nadie va a mirar. El error nunca consistió en usar esta política; consistió en usarla por defecto, extendida a datos que no cumplen ninguna de las tres condiciones, como el texto que alguien acaba de escribir.
La prueba que separa ambos territorios se puede formular en una sola frase y aplicarla campo a campo lleva minutos: pregúntate si al usuario le importaría recuperar el valor descartado. Si la respuesta es que ni siquiera sabría qué era —la posición del cursor de hace un minuto, la batería de ayer—, la política es adecuada y no hay más que hablar. Si la respuesta es que lo reclamaría, la política es inadecuada por mucho que converja, y el hecho de que hasta hoy no se haya quejado nadie solo significa que aún no ha ocurrido el caso o que quien lo sufrió no tuvo forma de saber a qué culpar.
Muestra de una realidad externa
Ubicación, sensor, presencia, batería. El valor no es una opinión de nadie: es una lectura, y la más reciente es la única válida.
Sin valor residual
Nadie querría recuperar el valor anterior ni sabría reconocerlo. Si alguien lo reclamaría, la condición no se cumple.
Escritura atómica y completa
El escritor no partió de un estado que había leído. Si su escritura era una edición incremental, descartarla destruye una intención.
Lo que nunca cumple las tres
Texto redactado por una persona, listas curadas, campos con historia detrás. Ahí la política no es un atajo: es una pérdida programada.
El giro que hay que dar aquí es de los que reorganizan la manera de leer un sistema entero, y cuesta darlo porque va contra una asociación que la literatura de sistemas distribuidos nos ha metido en el cuerpo durante veinte años. Un registro de último escritor no tiene ningún defecto formal: es un tipo de dato replicado sin conflictos en el sentido técnico estricto, su función de fusión forma un semirretículo con las tres propiedades exigidas, y se puede demostrar que dos réplicas cualesquiera que hayan intercambiado sus estados terminan idénticas. Es, literalmente, correcto. Y sin embargo pierde el trabajo de la gente todos los días. La única forma de sostener ambas afirmaciones sin contradicción es reconocer que estábamos midiendo con una sola vara un objeto que tiene dos dimensiones independientes. La convergencia es una propiedad de la relación entre las réplicas: pregunta si todos los nodos acaban viendo lo mismo. La preservación es una propiedad de la relación entre el sistema y sus usuarios: pregunta si el estado final conserva la intención de quienes escribieron. Nada en la definición de la primera implica la segunda, y la prueba es inmediata y demoledora: una función de fusión que descarte ambos valores y devuelva siempre la cadena vacía es conmutativa, asociativa e idempotente, converge con la misma elegancia formal y destruye el cien por cien de los datos. Que ese sistema absurdo pase todas las pruebas que le exigimos a un motor de sincronización debería bastar para convencer a cualquiera de que las pruebas estaban incompletas. El motivo por el que la confusión está tan extendida es histórico y merece nombrarse: la conversación pública sobre datos distribuidos se organizó durante dos décadas alrededor del teorema CAP y de la elección entre consistencia y disponibilidad, y en ese marco consistencia significa acuerdo entre réplicas y nada más. Un sistema que consigue acuerdo se considera resuelto, se marca la casilla y la discusión pasa al rendimiento. La pregunta y qué le pasó a lo que el otro había escrito no cabe en ese vocabulario, porque el teorema no la formula y las herramientas construidas bajo su influencia no la miden. La consecuencia práctica para quien diseña una aplicación local-first es doble y bastante exigente. Primero, deja de aceptar converge como respuesta a funciona: son preguntas distintas y la segunda solo se responde examinando qué se descarta y quién se entera. Segundo, entiende que el paso del gana-el-último a lo que viene después no es una mejora de precisión ni una optimización, sino la aparición de un requisito nuevo que nunca habías escrito en ninguna especificación: que la intención de un usuario no desaparezca por el hecho de haber estado desconectado. Todo el resto de este track —las alternativas de la próxima lección, los quince niveles de estructuras convergentes que vienen después— es la respuesta técnica a ese requisito. Pero el requisito hay que ponerlo primero, porque ninguna cantidad de matemáticas lo deduce solo.
- Localiza cada punto de tu sistema donde una escritura sobrescribe a otra por marca de tiempo y anota qué dato concreto está en juego en cada uno.
- Clasifica esos datos según las tres condiciones de la última sección y separa los que legítimamente admiten la política de los que la heredaron por defecto.
- Adelanta el reloj de un dispositivo dos minutos, edita desde él y desde otro sin conexión, sincroniza y comprueba quién gana de forma sistemática.
- Retrasa el reloj de un dispositivo y verifica si sus escrituras posteriores quedan permanentemente incapaces de sobrescribir a las anteriores.
- Borra un registro en una réplica mientras otra desconectada lo modifica, reconecta y comprueba si el dato resucita.
- Añade el registro del descartado sin cambiar la política y mide durante una semana cuántas escrituras estabas destruyendo sin saberlo.