18 dic 2025·8 min de lectura

Bloqueo optimista para evitar actualizaciones perdidas en aplicaciones web

Aprende cómo el bloqueo optimista evita actualizaciones perdidas cuando dos pestañas o usuarios editan el mismo registro, usando columnas de versión o ETags y un manejo claro de conflictos.

Bloqueo optimista para evitar actualizaciones perdidas en aplicaciones web

El problema de las actualizaciones perdidas en lenguaje sencillo

Una “actualización perdida” ocurre cuando dos personas (o dos pestañas del navegador) editan lo mismo y la segunda guardada sobrescribe silenciosamente a la primera. Nadie ve un error. Ambos usuarios creen que su cambio se guardó. Pero solo la última guardada queda en la base de datos.

Un ejemplo simple: abres tu perfil en una pestaña y cambias el nombre visible de “Sam” a “Samantha”, pero aún no haces clic en Guardar. En otra pestaña, cambias tu correo y haces clic en Guardar. Luego vuelves a la primera pestaña y haces clic en Guardar. Si la app usa “la última escritura gana”, esa suma anterior puede sobrescribir el cambio más reciente en el correo, aunque nunca tocaste ese campo en la primera pestaña.

Esto suele pasar desapercibido porque todo parece normal. El servidor devuelve “200 OK”, la UI muestra un toast de éxito y la página se refresca. El bug solo aparece después, cuando alguien se da cuenta de que una configuración volvió atrás, una dirección cambió o una actualización de administrador desapareció. Para entonces parece aleatorio y los clientes pueden culpar al software de ser “inestable”.

Lo verás sobre todo en pantallas CRUD comunes que la gente deja abiertas un rato: perfiles y ajustes de cuenta, paneles de administración (usuarios, productos, permisos), páginas de configuración de equipo (roles, facturación), editores de contenido (títulos, metadatos, descripciones) y cualquier formulario de edición que cargue datos una vez y guarde después.

“Última escritura gana” es arriesgado porque trata cada guardado como igualmente vigente, incluso cuando se basa en datos obsoletos. También crea un problema de confianza: los usuarios hicieron lo correcto, pero el sistema tiró su trabajo en silencio.

El bloqueo optimista es una forma común de evitar esto. Cuando guardas, el servidor comprueba si el registro cambió desde que lo cargaste. Si cambió, la app detiene la sobrescritura y te pide resolver el conflicto en vez de fingir que todo está bien.

Situaciones comunes que causan sobrescrituras silenciosas

Las sobrescrituras silenciosas ocurren cuando tu app deja a alguien editar datos basándose en un snapshot antiguo y luego guarda sin comprobar qué cambió entre medias. El resultado es “la última guardada gana”, incluso cuando esa última guardada es la incorrecta.

La causa más habitual son dos pestañas (o ventanas) abiertas en la misma página. Actualizas un registro en la Pestaña A, olvidas que la Pestaña B sigue abierta y luego la Pestaña B guarda después y, sin saberlo, vuelve a poner valores antiguos. Esto aparece mucho en paneles de administración, dashboards y páginas de "Editar perfil" que la gente deja abiertas horas.

También ocurre cuando dos personas diferentes trabajan el mismo registro. Piensa en una nota de cliente, el estado de un pedido o una dirección. Persona 1 cambia el teléfono, Persona 2 cambia las instrucciones de entrega y quien haga clic en Guardar último puede borrar el cambio del otro si la app envía todo el registro en vez de solo los campos modificados.

Redes lentas o inestables empeoran esto. Un guardado desde un tren o con conexión móvil intermitente puede llegar tarde. Si tu servidor lo acepta como actual, puede sobrescribir ediciones más recientes que se guardaron mientras la primera petición estaba en vuelo. El modo offline tiene el mismo riesgo: puedes editar localmente, volver en línea y empujar cambios que ahora están desfasados.

Los reintentos también pueden re-enviar una actualización vieja: un usuario hace doble clic en Guardar, un job en background reintenta después de un timeout usando datos antiguos, una cola reproduce un mensaje tras un crash, o una librería cliente reintenta automáticamente una petición que ya se aplicó.

Estos son precisamente los casos donde el bloqueo optimista se paga solo. Añade una simple comprobación de versión (o un chequeo ETag) y el servidor podrá detectar “editaste una copia antigua” y rechazar el guardado en lugar de sobrescribir en silencio.

Qué es el bloqueo optimista y cómo funciona

El bloqueo optimista evita el problema de actualizaciones perdidas. En vez de bloquear a las personas para que nadie edite, permite que todos editen libremente y solo comprueba conflictos cuando alguien intenta guardar.

Es más fácil de entender comparándolo con el bloqueo pesimista. El bloqueo pesimista es como poner un letrero físico de "no tocar" en un registro mientras lo editas. Previene conflictos, pero también crea esperas, timeouts y el problema de “alguien dejó la página abierta”. El bloqueo optimista asume que los conflictos son raros, así que evita bloquear y solo detiene el guardado cuando realmente ocurre un conflicto.

Una “versión” es un dato pequeño que cambia cada vez que una fila o documento cambia. En una fila de base de datos suele ser una columna entera como version que empieza en 1 y se incrementa en cada actualización. En APIs también puede ser un ETag, que es una huella del estado actual.

El flujo básico es:

  • Cuando cargas un registro, también lees su versión actual.
  • Cuando guardas, envías de vuelta la versión que viste originalmente.
  • La actualización tiene éxito solo si la versión almacenada sigue coincidiendo.
  • Si coincide, el registro se actualiza y la versión se incrementa.
  • Si no coincide, el guardado se rechaza como conflicto.

Esa discrepancia es el punto: el sistema te dice “Alguien (o otra pestaña) cambió esto después de que lo cargaste.” La app puede entonces mostrar un mensaje claro y ofrecer un paso seguro siguiente, como recargar, revisar diferencias o copiar tus cambios antes de reintentar. La sobrescritura nunca ocurre de forma silenciosa.

Esto encaja con la mayoría de apps CRUD porque la mayoría de los usuarios no están editando exactamente el mismo registro al mismo tiempo. Mantienes la UI responsiva (sin locks activos mientras alguien piensa) y aún proteges los datos.

Un ejemplo mental rápido: abres un formulario de perfil en dos pestañas. La Pestaña A guarda primero, subiendo la versión de 3 a 4. La Pestaña B intenta guardar con la versión 3. La base de datos (o la API) rechaza y la Pestaña B tiene que refrescar o hacer merge. Esa pequeña comprobación de versión convierte una pérdida de datos oculta en un conflicto visible y solucionable.

Paso a paso: enfoque con columna de versión

Una columna de versión es la forma más sencilla de detener sobrescrituras silenciosas en una app CRUD. Guarda un número en cada fila, envíalo al cliente cuando lea el registro y exige que el cliente lo devuelva al guardar. Si el número cambió desde que cargó la página, rechaza la actualización.

1) Añadir un campo de versión a la tabla

Añade una columna entera, a menudo llamada version, empezando en 1. (Usar updated_at puede funcionar como respaldo, pero las marcas de tiempo pueden complicarse con zonas horarias y ediciones muy rápidas.)

ALTER TABLE documents ADD COLUMN version INTEGER NOT NULL DEFAULT 1;

2) Llevar la versión a través de tu API

Haz que la versión viaje con el registro de extremo a extremo. En la lectura, incluye version en la respuesta de la API para que la UI pueda guardarla. En la edición, mantén esa versión en el estado del formulario (aunque esté oculta). En la actualización, exige que el cliente envíe la última versión vista (el cuerpo de la petición es lo más sencillo).

Ahora el servidor puede saber si el cliente está guardando una copia antigua.

3) Actualizar solo si la versión sigue coincidiendo

Este es el núcleo del bloqueo optimista: actualiza la fila solo cuando id y version coinciden, y luego incrementa la versión.

Un patrón común es una consulta atómica:

UPDATE documents
SET title = ?, body = ?, version = version + 1
WHERE id = ? AND version = ?;

Si la consulta actualiza 0 filas, la versión no coincidió. Devuelve un conflicto (a menudo HTTP 409) e incluye el registro más reciente (y su nueva versión) para que la UI muestre qué cambió.

4) Incrementar al tener éxito, rechazar en caso de discrepancia

Cuando el guardado tiene éxito, la respuesta debería incluir la nueva versión para que el cliente esté listo para la siguiente edición. Cuando falla, no reintentes automáticamente. Eso puede convertir una señal clara de “editaste una copia vieja” en más confusión y aún puede llevar a sobrescrituras.

Paso a paso: usar ETags con encabezados If-Match

Reparar un proyecto IA roto
¿Heredaste una app generada por IA? Repararemos la base de código y verificaremos cada arreglo a mano.

Un ETag es una huella breve que representa el estado actual de un recurso. Si el recurso cambia, la huella cambia también. Eso lo hace adecuado para bloqueo optimista cuando no quieres añadir una columna de versión, o cuando quieres que el servidor decida qué significa “mismo estado”.

1) Devolver un ETag en la lectura (GET)

Cuando un cliente carga un registro para editar, tu API debería devolver el registro más un ETag que coincida con ese estado exacto. Puedes calcular el ETag a partir de una versión de fila, un updated_at o un hash del JSON que devuelves.

Un flujo simple:

  • El cliente envía GET /items/123
  • El servidor responde con el body JSON y un encabezado ETag: "abc123"
  • El cliente guarda ese ETag junto al formulario que está editando

2) Exigir If-Match en la escritura (PUT/PATCH)

Cuando el usuario guarda, el cliente incluye el ETag que vio originalmente. Eso le dice al servidor: “Aplica mi actualización solo si el recurso todavía está en el estado que edité.”

  • Cliente envía PATCH /items/123 con el encabezado If-Match: "abc123"
  • El servidor compara If-Match con el ETag actual del item 123
  • Si coinciden, aplica el cambio y devuelve el recurso actualizado con un nuevo ETag

Si no coinciden, hay un conflicto. No aceptes la escritura silenciosamente.

3) Devolver la respuesta correcta en caso de conflicto

La mayoría de las APIs devuelven 412 Precondition Failed cuando If-Match no coincide (lo más preciso), o 409 Conflict si prefieres una respuesta más general.

Incluye suficiente detalle para que el cliente se recupere: un código/fecha corto, y a menudo la versión más reciente del recurso (más su nuevo ETag) para que la UI pueda mostrar lo que cambió.

Cuando los ETags encajan mejor que una columna de versión

Los ETags son útiles cuando no puedes cambiar fácilmente el esquema de la base de datos, cuando varios backends pueden actualizar el mismo recurso o cuando ya usas semánticas de caché. También funcionan bien cuando el “estado del recurso” no es una sola fila, como un documento ensamblado desde varias tablas.

Ejemplo: dos pestañas editan el mismo perfil. La Pestaña A carga la página y recibe ETag "v1". La Pestaña B guarda un cambio primero, haciendo que el ETag del perfil sea "v2". Cuando la Pestaña A intenta guardar con If-Match: "v1", el servidor devuelve 412. La UI puede entonces pedir al usuario que recargue o mostrar una pequeña pantalla de fusión en vez de sobrescribir la Pestaña B.

Cómo manejar conflictos sin frustrar a los usuarios

Un conflicto no es un error desde el punto de vista del usuario. Es una sorpresa. Tu trabajo es explicar lo que pasó y ayudarles a conservar su trabajo.

Usa un mensaje claro que nombre el problema y su impacto: “Este registro se cambió en otro lugar mientras lo editabas. Tus cambios no se guardaron todavía.” Evita textos vagos como “409 Conflict” o “Actualización fallida”. La gente necesita saber que no fue su culpa.

Ofrece opciones simples (y haz la opción segura la más fácil)

La mayoría de apps necesitan las mismas tres opciones. Manténlas simples y haz que la ruta más segura sea la predeterminada.

  • Recargar la versión más reciente: traer la versión más nueva y mostrarla.
  • Mantener mis ediciones: conservar la entrada no guardada del usuario en el formulario para que pueda re-aplicarla.
  • Sobrescribir de todos modos: permitirlo solo cuando el usuario confirme claramente que quiere reemplazar el cambio de otra persona.

Haz que “Recargar la versión más reciente” sea la acción primaria. “Sobrescribir de todos modos” debe ser secundaria y explícita, con una confirmación que diga qué se va a sobrescribir.

Conserva la entrada no guardada del usuario

La forma más rápida de perder la confianza es borrar un formulario después de un conflicto. Conserva lo que escribieron, incluso si recargas el registro.

Un patrón práctico: guarda las ediciones pendientes del usuario por separado (estado local o borrador), recarga los datos más recientes del servidor y luego vuelve a poblar el formulario usando los valores del borrador donde aún tengan sentido. Si puedes, destaca los campos que difieren de la versión del servidor para que el usuario detecte rápido qué cambió.

Cuando recargar basta vs cuando necesitas una UI de fusión

Una recarga simple es suficiente cuando el formulario es corto, los cambios suelen ser pequeños y el coste de reescribir es bajo.

Probablemente necesites una UI de fusión cuando los usuarios editan texto largo (descripciones, notas, políticas), cuando muchos campos pueden cambiar a la vez (tablas de precios, configuraciones multi-paso) o cuando los conflictos ocurren con frecuencia (equipos concurridos, pantallas de administración compartidas).

Un flujo realista: dos personas editan el mismo registro de cliente. Una actualiza el teléfono y guarda. La otra cambia la dirección y guarda después. Con buen manejo de conflictos, la segunda persona ve: “El número de teléfono cambió por otra persona. Tu edición de la dirección sigue aquí.” Puede recargar y volver a guardar sin reescribir todo.

Errores comunes y trampas a evitar

Maneja conflictos sin frustrar a los usuarios
Implementaremos manejo de conflictos que mantiene borradores de usuario y explica qué cambió.

El bloqueo optimista es simple en teoría, pero algunos errores pueden anular todo el propósito. La mayoría aparecen solo cuando usuarios reales empiezan a trabajar en múltiples pestañas, o cuando se añade una nueva ruta de código durante un lanzamiento apresurado.

Trampas que causan sobrescrituras silenciosas

  • Usar updated_at como la “versión” cuando las marcas temporales no son lo bastante precisas. Si dos actualizaciones caen en el mismo segundo (o la base de datos redondea), ambas pueden verse válidas y una puede sobrescribir a la otra.
  • Añadir la comprobación de versión/ETag en un endpoint pero olvidarla en otro. Por ejemplo, la pantalla principal de edición usa la comprobación, pero un toggle rápido, un autosave, un panel de admin o un job en background actualizan el mismo registro sin ella.
  • Cambiar la versión en lecturas, o en escrituras que no modifican campos gestionados por el usuario. Si incrementas la versión cuando alguien solo ve la página, creas conflictos que se sienten aleatorios e injustos.
  • Capturar el conflicto y reintentar automáticamente sin intervención del usuario. Los reintentos ciegos pueden convertir una señal clara de “editaste una copia vieja” en un bucle confuso.
  • Hacer actualizaciones en lote que omiten la comprobación de concurrencia. Una sola sentencia SQL o una herramienta batch que actualiza muchas filas puede ignorar la condición de versión y borrar ediciones recientes.

Pequeños hábitos que previenen grandes bugs

Sé consistente: si un registro es editable, cada ruta de escritura debería (1) exigir la versión/ETag, o (2) estar diseñada explícitamente como una sobreescritura forzada y registrada como tal.

Mantén la versionación estable. La versión solo debería avanzar cuando aceptas una actualización que se basó en la versión más reciente conocida. Si tu app tiene escrituras por efectos secundarios (recalcular contadores, sincronizar metadatos, ajustar last_seen), considera moverlas a otra tabla para que no creen conflictos de edición innecesarios.

Una comprobación rápida de realidad: abre el mismo formulario de edición en dos pestañas, guarda en la pestaña A y luego guarda en la pestaña B. Si la pestaña B tiene éxito sin un conflicto claro, aún tienes un camino de actualización perdida en algún sitio.

Chequeos rápidos antes de lanzar

Trata el bloqueo optimista como una característica que puedes romper a propósito. Si dos guardados compiten, deberías obtener un conflicto claro en vez de una sobrescritura silenciosa.

Empieza con la prueba del mundo real más fácil: abre el mismo registro en dos pestañas del navegador. Cambia campos distintos en cada pestaña, guarda en la pestaña A y luego en la B. La pestaña B no debería “ganar” en silencio. Debería recibir una respuesta de conflicto (a menudo HTTP 409) y un mensaje que diga a la app qué pasó.

Las redes lentas son donde se esconden estos bugs. Usa el throttling de red del navegador (o añade un retardo artificial en el servidor) para que un guardado tarde unos segundos. Mientras está en vuelo, guarda desde otra pestaña. Cuando la petición retrasada finalmente vuelva, debe fallar de forma segura.

Una lista de comprobación rápida antes de enviar:

  • Dos pestañas: edita el mismo registro, guarda en ambas, confirma que el segundo guardado recibe un conflicto.
  • Guardado lento: retrasa una petición, guarda otra, confirma que la retrasada es rechazada.
  • Reanudar en móvil: edita, manda la app a segundo plano, vuelve y guarda, confirma que la versión/ETag se sigue comprobando.
  • Recuperación offline: pierde conexión a mitad de edición, reconecta y guarda, confirma que manejas conflictos en vez de sobrescribir.
  • Seguridad de borradores: tras un conflicto, confirma que la UI conserva el texto no guardado del usuario.

También verifica los detalles que importan a los usuarios. Cuando ocurre un conflicto, la respuesta debería ser lo bastante específica para que tu cliente reaccione: un estado claro, un mensaje legible por humanos y, idealmente, la versión más reciente del servidor para que la UI pueda mostrar “tus cambios” vs “versión actual”.

Un ejemplo realista: dos personas editando los mismos datos

Deja el código IA listo para producción
FixMyMess convierte prototipos generados por IA en apps listas para producción con reglas seguras de actualización.

Dos compañeros, Maya y Jordan, están actualizando la misma regla de precios en un panel de administración. La regla dice: “10% de descuento cuando el total del carrito es más de $100.” Maya quiere cambiarlo a $120. Jordan quiere cambiar el descuento a 15%.

Ambos abren la página de edición a las 10:00. Cada pestaña carga la regla actual. En ese momento, el registro tiene version = 7 (o un valor equivalente).

Qué pasa sin bloqueo

Maya guarda primero a las 10:02. El servidor escribe “threshold = 120” en la base de datos.

Jordan guarda a las 10:03. Su navegador todavía tiene los valores del formulario antiguos, así que su actualización escribe “discount = 15%” y además envía el valor de threshold antiguo desde cuando cargó la página. El resultado es una sobrescritura silenciosa: el cambio de Maya en el threshold desaparece y nadie recibe una advertencia. La UI suele mostrar “Guardado” en ambos casos, así que el equipo confía en datos equivocados.

Qué pasa con una columna de versión

Con bloqueo optimista, ambas actualizaciones incluyen la versión desde la que empezaron.

  • La petición de Maya dice: “update this rule where id=123 and version=7”
  • El servidor actualiza la fila y la sube a version 8
  • La petición de Jordan también dice: "where version=7"
  • La base de datos no encuentra coincidencia (porque ahora la fila es version 8)
  • El servidor devuelve una respuesta de conflicto en vez de sobrescribir

Lo que ve el usuario: Jordan recibe un mensaje claro como “Esta regla de precios fue cambiada por otra persona. Revisa la versión más reciente antes de guardar.” La página recarga los datos más nuevos (threshold 120, version 8). Las ediciones no guardadas de Jordan pueden mantenerse localmente para que pueda volver a aplicar “15%” y guardar de nuevo.

Qué datos se preservan: el registro guardado más reciente queda intacto y el cambio que Jordan quería no se pierde; se retrasa hasta que él lo confirme frente a la versión más nueva.

Siguientes pasos: desplegarlo con seguridad (y pedir ayuda si hace falta)

Empieza eligiendo el enfoque que encaje con cómo trabaja ya tu app. Una columna de versión suele ser lo más sencillo cuando controlas la base de datos y el ORM y la mayoría de las actualizaciones pasan por tu servidor. Los ETags con If-Match son una buena opción cuando tienes una API REST limpia, varios clientes o necesidades de caché fuerte.

Despliega en pequeños pasos. Elige un flujo de edición de alto valor (perfiles, pedidos, ajustes) y añade bloqueo optimista de extremo a extremo: lectura, edición, actualización y un mensaje de conflicto claro. Una vez que ese camino esté sólido, repite con el siguiente recurso.

Una lista de despliegue segura:

  • Añade la versión (o ETag) a cada respuesta de lectura y a cada petición de actualización.
  • Devuelve una respuesta de conflicto clara cuando las versiones no coincidan (nada de sobrescrituras silenciosas).
  • Muestra una elección simple en la UI: recargar, mantener tus cambios o reintentar tras revisar.
  • Registra los conflictos con tipo de recurso y frecuencia para identificar puntos calientes.
  • Añade una o dos pruebas que simulen dos pestañas actualizando el mismo registro.

No te pares en la API. Si la UI solo dice “Guardado fallido”, la gente reintentará y aún podrá sobrescribir cambios de otros. Dales contexto: qué cambió y qué pueden hacer a continuación. Para formularios simples, un buen comportamiento por defecto es recargar los datos más recientes y conservar el borrador del usuario para que pueda volver a aplicar los cambios.

Si heredaste una app generada por IA, vale la pena comprobar que cada ruta de actualización siga la misma regla de concurrencia. Equipos como FixMyMess (fixmymess.ai) se enfocan en convertir prototipos generados por IA en software listo para producción, y una auditoría rápida suele encontrar checks de versión o ETag faltantes antes de que causen pérdida real de datos.