wandres.dev
KV · clave-valor global

Consistencia eventual: el desfase de hasta 60 segundos

La propiedad más importante y peor entendida de Workers KV es su consistencia eventual: una escritura tarda hasta unos 60 segundos en propagarse a todos los centros de datos, y durante esa ventana una lectura lejana puede devolver el valor anterior. No es un bug ni una casualidad de implementación, sino el precio deliberado de servir lecturas globales a un milisegundo. Vemos qué significa exactamente ese desfase, por qué es una consecuencia inevitable del modelo de origen central más cachés en el edge, y sobre todo cómo diseñar aplicaciones que asumen el desfase en vez de pelearse con él.

⏱ 16 min

Aquí está la propiedad que hay que respetar más que ninguna otra: cuando escribes una clave en KV, el mundo no se entera de golpe. La escritura llega primero al origen y luego se propaga hacia los cientos de cachés repartidos por el planeta, y esa propagación tarda —hasta del orden de 60 segundos en el peor caso—. Durante esa ventana, un usuario en la otra punta del mundo puede leer y recibir todavía el valor viejo. A esto se le llama consistencia eventual, y quien lo ignora construye bugs que solo aparecen en producción, de forma intermitente y a distancia. Quien lo entiende diseña con el desfase incorporado y nunca lo sufre.

🎯 Al terminar esta lección sabrás
  • Precisar qué significa que KV sea eventualmente consistente y de qué orden es el desfase.
  • Entender por qué la propagación global no puede ser instantánea en el modelo origen-central más cachés.
  • Distinguir consistencia eventual de lo que NO garantiza KV: lectura tras escritura y escrituras concurrentes.
  • Diseñar aplicaciones que dan por hecho el desfase en vez de asumir que la escritura se ve al instante.

Qué significa “hasta 60 segundos”

Consistencia eventual quiere decir esto: si dejas de escribir una clave, con el tiempo todas las réplicas del planeta convergerán al último valor —eventualmente todas coincidirán—. Lo que no se garantiza es cuándo. La propagación desde el origen hacia los cachés del edge no es instantánea; en el peor caso puede rondar el minuto. Mientras tanto, dos lecturas simultáneas desde dos continentes distintos pueden devolver valores diferentes de la misma clave, y ambas son respuestas legítimas de KV.

Hay un matiz que suele confundir. Si escribes y lees desde el mismo centro de datos, es probable que veas tu propio cambio enseguida, porque el caché local ya se actualizó. Pero KV no promete ni siquiera eso: no ofrece la garantía de “lee tus propias escrituras” de forma fiable, porque tu segunda petición podría atenderla otro centro de datos, o el mismo tras haber vaciado la copia. La única postura segura es no asumir nada sobre el momento en que una escritura se hace visible en ningún sitio.

Converge, con el tiempo

Si dejas de escribir una clave, todas las réplicas del planeta acabarán coincidiendo en el último valor. La convergencia está garantizada; el instante en que ocurre, no.

No dice cuándo

El desfase entre escribir y que todos lo lean puede rondar los 60 segundos. Ninguna lectura se bloquea esperando: te da la copia que tiene, aunque sea de hace un rato.

🚫

No hay lectura tras escritura

No supongas que puedes leer al instante lo que acabas de escribir, ni siquiera en la misma región. Si un paso siguiente necesita el dato, pásalo por el camino, no lo releas de KV.

⚔️

Gana la última

Sin transacciones ni atomicidad, dos escrituras casi simultáneas a una clave se pisan: sobrevive la última en llegar al origen y la otra se pierde sin aviso.

Por qué el desfase es inevitable

El desfase no es pereza de la implementación: es geometría. KV guarda la verdad en un origen central y sirve copias desde cachés cercanos al usuario. Ese diseño es lo que hace que una lectura cueste un milisegundo en vez de cruzar el océano. Pero tiene una consecuencia forzosa: cuando el origen cambia, las copias ya repartidas siguen siendo válidas hasta que se enteran, y enterarse lleva tiempo porque hay cientos de ubicaciones y la luz no va infinitamente rápido.

sequenceDiagram
participant U as Worker en Madrid
participant O as Origen central
participant T as Cache en Tokio
U->>O: put clave igual a v2
Note over O: el origen ya tiene v2
T-->>T: aun sirve v1 en cache
Note over T: lectura desde Tokio devuelve v1
O-)T: propaga v2 en segundo plano
Note over T: tras hasta 60 s converge a v2

Podrías imaginar una alternativa: que cada put bloqueara hasta confirmar que las cientos de réplicas se han actualizado. Eso daría consistencia fuerte, pero destruiría justo aquello que hace útil a KV: la escritura pasaría de rápida a lentísima, y una partición de red en cualquier rincón del mundo bloquearía las escrituras de todos. KV elige lo contrario —propagar en segundo plano y no bloquear nunca la lectura—, y el desfase es la sombra que proyecta esa elección. No se puede tener a la vez lecturas globales instantáneas, escrituras baratas y consistencia inmediata; hay que renunciar a una, y KV renuncia a la última.

Diseñar dando por hecho el desfase

La buena noticia es que diseñar con consistencia eventual es sencillo una vez que aceptas la regla: nunca supongas que una escritura se ve inmediatamente en otro sitio, y nunca uses KV para coordinar. En la práctica esto se traduce en unos pocos patrones concretos.

El primero es preferir claves inmutables a claves mutables. En vez de sobrescribir config una y otra vez, escribe config:v3 y guarda en algún sitio un puntero a la versión activa; una clave que solo se escribe una vez no tiene problema de propagación porque nunca cambia. El segundo es no exigir lectura tras escritura: si un flujo escribe un dato y otro paso inmediato necesita ese dato, pásalo por el camino —en la respuesta, en el mensaje de una cola— en vez de reescribirlo y releerlo de KV. El tercero es tolerar el valor viejo por diseño: si una feature flag tarda 60 segundos en activarse en todo el mundo, que no pase nada; los datos de KV deben ser aquellos para los que “un poco desactualizado” es perfectamente aceptable.

// ANTIPATRON: sobrescribir y esperar leer el nuevo valor al instante
await env.MI_KV.put("contador", String(n + 1)); // ademas: last-write-wins
const v = await env.MI_KV.get("contador");        // puede devolver el viejo

// PATRON: claves versionadas e inmutables + un puntero a la version activa
await env.MI_KV.put("config:v3", JSON.stringify(nuevaConfig));
await env.MI_KV.put("config:activa", "v3"); // el puntero converge con calma
⚠️
Cuidado con las escrituras concurrentes: gana la última, sin avisar

La consistencia eventual trae una hermana peligrosa: KV no tiene transacciones ni operaciones atómicas. Si dos Workers escriben la misma clave casi a la vez, gana la última en llegar al origen y la otra se pierde en silencio, sin error ni aviso. Por eso get, sumar uno y put nunca es seguro como contador: entre tu lectura y tu escritura, otro pudo haber escrito, y su valor desaparece. Cualquier dato que dependa de “leer, modificar, escribir” de forma segura no pertenece a KV, sino a un Durable Object.

La consistencia no es verdadero o falso, es un espectro que eliges por dato

El error de fondo del principiante es creer que “consistente” y “no consistente” son dos categorías, y que lo segundo es una versión defectuosa de lo primero. La realidad de los sistemas distribuidos es que la consistencia es un espectro con un precio creciente, y elegir el punto correcto para cada dato es la esencia del diseño. En un extremo está la linealizabilidad —toda operación parece ocurrir en un instante único y global, como si hubiera un solo reloj para todo el planeta—; es la garantía más fuerte y la más cara, porque exige coordinación y, ante una partición, sacrificar disponibilidad. En el otro extremo está la consistencia eventual: cero coordinación, disponibilidad total, y a cambio la aceptación de que durante un rato distintos observadores ven distintas versiones de la verdad. KV vive deliberadamente en ese extremo barato porque la mayoría de los datos de lectura intensiva no necesitan más: que tu página de precios, tu tabla de rutas o tu bandera de funcionalidad tarden un minuto en cuadrar en todo el mundo no rompe absolutamente nada. La madurez consiste en dejar de preguntar “¿es esto consistente?” y empezar a preguntar “¿cuánta inconsistencia tolera este dato concreto, y durante cuánto tiempo?”. Un saldo bancario tolera cero y exige un sistema fuerte. Un contador de “me gusta” tolera mucho. Una feature flag tolera un minuto sin pestañear. Cuando clasificas tus datos por su tolerancia a la incoherencia temporal —en lugar de exigirle a todos la garantía más cara— descubres que la inmensa mayoría cabe holgadamente en el modelo de KV, y que reservar la consistencia fuerte para el puñado que de verdad la necesita no es una concesión, sino la asignación económica correcta de un recurso escaso. Los grandes diseños de datos no eligen una consistencia para todo el sistema: eligen la consistencia justa, dato por dato, y colocan cada uno en la pieza cuya física corresponde a esa necesidad. KV es donde viven, felices, todos los datos que pueden permitirse esperar.

⚔️ Piensa en términos de desfase
  1. Explica con tus palabras la diferencia entre “el origen ya tiene el valor nuevo” y “todo el mundo lo lee”: ¿por qué media un desfase y de qué orden es?
  2. Da un ejemplo de bug intermitente que surja de asumir lectura tras escritura en KV, y describe por qué solo aparecería para usuarios lejanos.
  3. Reescribe un flujo que hoy hace put seguido de get de la misma clave para que no dependa de leer lo que acaba de escribir.
  4. Convierte una clave mutable que se sobrescribe a menudo en un esquema de claves versionadas e inmutables con un puntero a la versión activa.
  5. Toma tres datos de tu app y clasifícalos por su tolerancia a la inconsistencia temporal; decide cuáles caben en KV y cuáles piden un Durable Object.