KoderKoder.ai
PreciosEmpresasEducaciónPara inversores
Iniciar sesiónComenzar

Producto

PreciosEmpresasPara inversores

Recursos

ContáctanosSoporteEducaciónBlog

Legal

Política de privacidadTérminos de usoSeguridadPolítica de uso aceptableReportar abuso

Social

LinkedInTwitter
Koder.ai
Idioma

© 2026 Koder.ai. Todos los derechos reservados.

Inicio›Blog›Condiciones de carrera en apps CRUD: causas y soluciones prácticas
20 oct 2025·8 min

Condiciones de carrera en apps CRUD: causas y soluciones prácticas

Las condiciones de carrera en apps CRUD pueden causar pedidos duplicados y totales erróneos. Aprende puntos comunes de colisión y soluciones prácticas con constraints, locks y protecciones en la interfaz.

Condiciones de carrera en apps CRUD: causas y soluciones prácticas

Cómo se ve una condición de carrera en una app CRUD

Una condición de carrera ocurre cuando dos (o más) peticiones actualizan los mismos datos casi al mismo tiempo, y el resultado final depende del orden y la sincronización. Cada petición parece correcta por sí sola. Juntas, producen un resultado erróneo.

Un ejemplo simple: dos personas hacen clic en Guardar sobre el mismo registro de cliente en el lapso de un segundo. Una actualiza el correo y la otra el teléfono. Si ambas solicitudes envían el registro completo, la segunda escritura puede sobrescribir la primera y un cambio desaparece sin error.

Esto ocurre más en apps rápidas porque los usuarios pueden disparar más acciones por minuto. También se dispara en momentos de alta carga: ventas flash, cierre de mes, una gran campaña de email, o cualquier momento en que muchas peticiones golpean las mismas filas.

Los usuarios rara vez reportan "una condición de carrera." Informan síntomas: pedidos o comentarios duplicados, actualizaciones perdidas ("Lo guardé y volvió atrás"), totales raros (inventario negativo, contadores que retroceden), o estados que cambian inesperadamente (aprobado y luego vuelve a pendiente).

Los reintentos lo empeoran. La gente hace doble clic, actualiza tras una respuesta lenta, envía desde dos pestañas o sufre redes inestables que hacen que navegadores o apps móviles reenvíen. Si el servidor trata cada petición como una escritura nueva, puedes obtener dos creaciones, dos cargos o dos cambios de estado que debían ocurrir una vez.

Por qué las condiciones de carrera pasan más a menudo de lo que parece

La mayoría de las apps CRUD parecen simples: leer una fila, cambiar un campo, guardarlo. El problema es que tu app no controla el tiempo. La base de datos, la red, los reintentos, trabajos en segundo plano y el comportamiento de los usuarios se solapan.

Un detonador común es que dos personas editan el mismo registro. Ambas cargan los mismos valores "actuales", ambas hacen cambios válidos y la última guardada sobrescribe silenciosamente la primera. Nadie hizo nada mal, pero una actualización se pierde.

También puede pasar con una sola persona. Un doble clic en Guardar, tocar atrás y adelante, o una conexión lenta que empuja a alguien a pulsar Enviar otra vez puede mandar la misma escritura dos veces. Si el endpoint no es idempotente, puedes crear duplicados, cobrar dos veces o avanzar un estado dos pasos.

El uso moderno añade más solapamientos. Varias pestañas o dispositivos con la misma cuenta pueden disparar actualizaciones en conflicto. Trabajos en segundo plano (emails, facturación, sincronización, limpieza) pueden tocar las mismas filas que las peticiones web. Reintentos automáticos en el cliente, balanceador o runner de trabajos pueden repetir una petición que ya tuvo éxito.

Si lanzas funcionalidades rápido, el mismo registro se actualiza desde más sitios de los que cualquiera recuerda. Si usas un creador guiado por chat como Koder.ai, la app puede crecer aún más rápido, así que vale tratar la concurrencia como comportamiento normal, no como un caso extremo.

Puntos comunes de colisión a vigilar

Las condiciones de carrera rara vez aparecen en demos de "crear un registro". Aparecen donde dos peticiones tocan la misma verdad casi al mismo momento. Saber los puntos calientes habituales te ayuda a diseñar escrituras seguras desde el día uno.

Contadores y campos de "siguiente número"

Todo lo que parezca "simplemente sumar 1" puede romperse bajo carga: likes, contadores de vistas, totales, números de factura, números de ticket. El patrón arriesgado es: leer el valor, sumar y luego escribirlo de nuevo. Dos peticiones pueden leer el mismo valor inicial y sobrescribirse mutuamente.

Transiciones de estado

Flujos como Draft -> Submitted -> Approved -> Paid parecen sencillos, pero las colisiones son comunes. El problema comienza cuando dos acciones son posibles a la vez (aprobar y editar, cancelar y pagar). Sin salvaguardas, puedes acabar con un registro que salta pasos, vuelve atrás o muestra distintos estados en diferentes tablas.

Trata los cambios de estado como un contrato: permite solo el siguiente paso válido y rechaza cualquier otro.

Inventario, capacidad y plazas reservables

Asientos disponibles, conteos de stock, huecos de cita y campos de "capacidad restante" crean el clásico problema de sobreventa. Dos compradores hacen checkout al mismo tiempo, ambos ven disponibilidad y ambos terminan con éxito. Si la base de datos no es el juez final, acabarás vendiendo más de lo que tienes.

Unicidad y reglas de "solo uno activo"

Algunas reglas son absolutas: un email por cuenta, una suscripción activa por usuario, un carrito abierto por usuario. Estas suelen fallar cuando primero verificas ("¿existe uno?") y luego insertas. Bajo concurrencia, ambas peticiones pueden pasar la comprobación.

Si estás generando flujos CRUD rápidamente (por ejemplo, chateando tu app en Koder.ai), apunta estos puntos calientes temprano y respáldalos con restricciones y escrituras seguras, no solo verificaciones en la interfaz.

Envios dobles y peticiones repetidas desde la interfaz

Muchas condiciones de carrera comienzan con algo mundano: la misma acción se envía dos veces. Los usuarios hacen doble clic. La red está lenta y vuelven a pulsar. Un teléfono registra dos toques. A veces no es intencional: la página se refresca tras un POST y el navegador ofrece reenviar el formulario.

Cuando eso ocurre, tu backend puede ejecutar dos creaciones o actualizaciones en paralelo. Si ambas tienen éxito, obtienes duplicados, totales erróneos o un cambio de estado que se ejecuta dos veces (por ejemplo, aprobar y luego aprobar otra vez). Parece aleatorio porque depende del tiempo.

El enfoque más seguro es defensa en profundidad. Arregla la interfaz, pero asume que la interfaz fallará.

Cambios prácticos que puedes aplicar a la mayoría de los flujos de escritura:

  • Desactivar el envío tras el primer clic y mostrar un estado claro de "guardando".
  • Redirigir después de un POST exitoso para que un refresco no reenvíe el mismo formulario.
  • Usar una clave de idempotencia: un token único por acción de usuario que el servidor guarda y acepta solo una vez.
  • Respaldarlo con una regla de unicidad en la base de datos para que dos inserts idénticos no puedan ganar ambos.

Ejemplo: un usuario pulsa "Pagar factura" dos veces en móvil. La UI debería bloquear el segundo toque. El servidor también debería rechazar la segunda petición al ver la misma clave de idempotencia, devolviendo el resultado original en lugar de cobrar dos veces.

Transiciones de estado que se desincronizan

Los campos de estado parecen simples hasta que dos cosas intentan cambiarlos a la vez. Un usuario pulsa Aprobar mientras un trabajo automático marca el mismo registro como Expirado, o dos miembros del equipo trabajan el mismo ítem en pestañas distintas. Ambas actualizaciones pueden tener éxito, pero el estado final depende del orden, no de tus reglas.

Trata el estado como una pequeña máquina de estados. Mantén una tabla corta de movimientos permitidos (por ejemplo: Draft -> Submitted -> Approved, y Submitted -> Rejected). Luego cada escritura verifica: "¿Está permitida esta transición desde el estado actual?" Si no, recházala en lugar de sobrescribir silenciosamente.

El bloqueo optimista te ayuda a detectar actualizaciones obsoletas sin bloquear a otros usuarios. Añade un número de versión (o updated_at) y exige que coincida al guardar. Si alguien cambió la fila después de que la cargaste, tu actualización afecta a cero filas y puedes mostrar un mensaje claro como "Este elemento cambió, actualiza y vuelve a intentarlo."

Un patrón simple para actualizaciones de estado es:

  • Leer estado y versión actuales
  • Validar la transición
  • Actualizar con una condición (WHERE status = ? AND version = ?)
  • Incrementar la versión

Además, concentra los cambios de estado en un solo lugar. Si las actualizaciones están repartidas entre pantallas, jobs en segundo plano y webhooks, perderás una regla. Ponlas detrás de una única función o endpoint que haga cumplir las mismas comprobaciones cada vez.

Contadores y totales que derivan con el tiempo

Planifica la concurrencia desde el principio
Mapea cada cambio de estado y cada escritura idempotente paso a paso antes de generar código.
Open Planning

El bug de contador más común parece inofensivo: la app lee un valor, suma 1 y lo escribe de nuevo. Bajo carga, dos peticiones pueden leer el mismo número y ambas escribir el mismo nuevo número, por lo que un incremento se pierde. Es fácil pasarlo por alto porque "normalmente funciona" en pruebas.

Prefiere actualizaciones atómicas en lugar de leer y luego escribir

Si un valor solo se incrementa o decrementa, deja que la base de datos lo haga en una sola sentencia. Entonces la base de datos aplica los cambios de forma segura incluso cuando muchas peticiones llegan a la vez.

UPDATE posts
SET like_count = like_count + 1
WHERE id = $1;

La misma idea aplica a inventario, contadores de vistas, contadores de reintento y cualquier cosa que pueda expresarse como "nuevo = viejo + delta".

Los totales se desvían cuando almacenas lo que podrías calcular

Los totales suelen fallar cuando guardas un número derivado (order_total, account_balance, project_hours) y luego lo actualizas desde varios lugares. Si puedes calcular el total a partir de filas fuente (líneas de pedido, asientos en libro mayor), evitas una clase entera de bugs.

Cuando debes almacenar un total por rendimiento, trátalo como una escritura crítica. Mantén las actualizaciones de filas fuente y del total almacenado en la misma transacción. Asegura que solo un escritor pueda actualizar el mismo total a la vez (bloqueos, actualizaciones condicionadas o una ruta de propietario único). Añade constraints que impidan valores imposibles (por ejemplo, inventario negativo). Luego reconcilia ocasionalmente con una comprobación en background que recalcule y marque discrepancias.

Un ejemplo concreto: dos usuarios añaden items al mismo carrito al mismo tiempo. Si cada petición lee cart_total, suma el precio y escribe, una de las adiciones puede desaparecer. Si actualizas los items del carrito y el total del carrito juntos en una transacción, el total se mantiene correcto incluso bajo clics paralelos.

Para de las colisiones con restricciones en la base de datos primero

Si quieres menos condiciones de carrera, empieza por la base de datos. El código de la app puede reintentar, expirar o ejecutarse dos veces. Una restricción en la base de datos es la última barrera que permanece correcta incluso cuando dos peticiones llegan a la vez.

Las restricciones únicas evitan duplicados que "nunca deberían ocurrir" pero ocurren: direcciones de email, números de pedido, IDs de factura o la regla "una suscripción activa por usuario". Cuando dos inscripciones llegan juntas, la base de datos acepta una fila y rechaza la otra.

Las claves foráneas previenen referencias rotas. Sin ellas, una petición puede borrar un registro padre mientras otra crea un hijo que apunta a nada, dejando filas huérfanas difíciles de limpiar.

Los check constraints mantienen valores dentro de un rango seguro y hacen cumplir reglas simples de estado. Por ejemplo, quantity >= 0, rating entre 1 y 5, o status limitado a un conjunto permitido.

Trata las fallas de constraints como resultados esperados, no como "errores de servidor." Captura violaciones de unicidad, foreign key y check, devuelve un mensaje claro como "Ese correo ya está en uso," y registra detalles para depurar sin filtrar información interna.

Ejemplo: dos personas hacen clic en "Crear pedido" dos veces durante lag. Con una restricción única en (user_id, cart_id) no obtendrás dos pedidos. Obtendrás un pedido y un rechazo limpio y explicable.

Usa transacciones y locks cuando un cambio debe ganar

Algunas escrituras no son una sola sentencia. Lees una fila, verificas una regla, actualizas un estado y quizá insertas un registro de auditoría. Si dos peticiones hacen eso a la vez, ambas pueden pasar la comprobación y ambas escribir. Ese es el patrón clásico de fallo.

Encierra la escritura multinivel en una transacción para que todos los pasos tengan éxito juntos o ninguno lo haga. Más importante, la transacción te da un lugar para controlar quién puede cambiar la misma data al mismo tiempo.

Cuando solo un actor puede editar un registro a la vez, usa un bloqueo a nivel de fila. Por ejemplo: bloquea la fila del pedido, confirma que sigue en estado "pending", luego cámbiala a "approved" y escribe la entrada de auditoría. La segunda petición esperará, volverá a comprobar el estado y se detendrá.

Bloqueo optimista vs pesimista

Elige según la frecuencia de colisiones:

  • Bloqueo optimista: añade un número de versión (o updated_at) y actualiza solo si coincide.
  • Bloqueo pesimista: bloquea la fila primero, luego lee y actualiza dentro de la misma transacción.

Mantén el tiempo de lock corto. Haz lo mínimo mientras lo sostienes: no llames a APIs externas, no hagas trabajos lentos ni bucles pesados. Si estás construyendo flujos en una herramienta como Koder.ai, deja la transacción solo para los pasos de base de datos y haz el resto después del commit.

Paso a paso: endurece una ruta de escritura de extremo a extremo

Evita sobreventas y desfases
Construye un flujo de pedidos e inventario donde el stock no pueda quedar negativo durante comprobaciones paralelas.
Create Flow

Elige un flujo que pueda perder dinero o confianza cuando colisione. Uno común es: crear un pedido, reservar stock y luego marcar el pedido como confirmado.

Escribe los pasos exactos que tu código realiza hoy, en orden. Sé específico sobre qué se lee, qué se escribe y qué significa "éxito". Las colisiones se esconden en la brecha entre una lectura y una escritura posterior.

Una ruta de hardening que funciona en la mayoría de stacks:

  • Define los malos resultados que debes bloquear (por ejemplo: stock negativo o dos reservas para la misma línea de pedido).
  • Añade una regla de base de datos que haga imposible ese resultado (por ejemplo: CHECK que impida stock < 0 y UNIQUE en (order_id, sku) para reservas).
  • Encierra todo el flujo en una transacción y bloquea las filas que deciden el resultado (por ejemplo, bloquea la fila de inventario del producto antes de descontar stock).
  • Realiza la actualización en una sola sentencia cuando puedas (por ejemplo: "update stock where stock >= qty" para que falle de forma segura).
  • Maneja los conflictos limpiamente: reintenta un par de veces, o devuelve un mensaje claro "Agotado, por favor actualiza".

Añade una prueba que demuestre la corrección. Ejecuta dos peticiones al mismo tiempo contra el mismo producto y cantidad. Asegura que exactamente un pedido queda confirmado y el otro falla de forma controlada (sin stock negativo, sin filas de reserva duplicadas).

Si generas apps rápido (incluyendo con plataformas como Koder.ai), esta checklist sigue valiendo para las pocas rutas de escritura que importan.

Errores comunes que mantienen vivas las condiciones de carrera

Una de las mayores causas es confiar en la UI. Botones deshabilitados y cheques en cliente ayudan, pero los usuarios pueden hacer doble clic, refrescar, abrir dos pestañas o reproducir una petición desde una conexión inestable. Si el servidor no es idempotente, los duplicados se filtran.

Otro bug silencioso: capturas un error de base de datos (como una violación de unicidad) pero continúas el flujo de trabajo de todos modos. Eso suele convertirse en "creación falló, pero igual enviamos el email" o "el pago falló, pero igual marcamos el pedido como pagado." Una vez ocurren efectos secundarios, es difícil revertirlos.

Transacciones largas también son una trampa. Si mantienes una transacción abierta mientras llamas a email, pagos o APIs de terceros, mantienes locks más tiempo del necesario. Eso aumenta esperas, timeouts y la probabilidad de que las peticiones se bloqueen entre sí.

Mezclar trabajos en segundo plano y acciones de usuario sin una única fuente de verdad crea estado dividido. Un job reintenta y actualiza una fila mientras un usuario la edita, y ambos creen que fueron los últimos en escribir.

Algunos "arreglos" que en realidad no lo hacen:

  • Prevención solo en UI contra envíos dobles
  • Ignorar un error y continuar como si nada pasara
  • Hacer llamadas de red lentas dentro de una transacción
  • Permitir que jobs y peticiones de usuario actualicen el mismo estado de manera distinta
  • Actualizar contadores con leer-modificar-escribir en lugar de actualizaciones atómicas

Si construyes con una herramienta chat-to-app como Koder.ai, las mismas reglas aplican: pide constraints en el servidor y límites transaccionales claros, no solo mejores protecciones en la UI.

Comprobaciones rápidas antes de lanzar

Haz las escrituras idempotentes
Diseña endpoints seguros ante reintentos para que los envíos dobles no creen registros extra.
Prevent Duplicates

Las condiciones de carrera suelen aparecer solo bajo tráfico real. Un repaso antes del lanzamiento puede atrapar los puntos de colisión más comunes sin reescribir todo.

Empieza por la base de datos. Si algo debe ser único (emails, números de factura, una suscripción activa por usuario), hazlo una restricción única real, no una regla de "comprobamos primero" en la app. Luego asegúrate de que tu código espere que la restricción falle a veces y devuelva una respuesta clara y segura.

Después, mira el estado. Cualquier cambio de estado (Draft -> Submitted -> Approved) debe validarse contra un conjunto explícito de transiciones permitidas. Si dos peticiones intentan mover el mismo registro, la segunda debería ser rechazada o convertirse en no-op, no crear un estado intermedio.

Una checklist práctica previa al lanzamiento:

  • Confirma que existen constraints únicos para cada regla que deba ser única, incluidas las compuestas.
  • Asegura que cada endpoint de escritura sea seguro ante reintentos (claves de idempotencia, IDs de petición o idempotencia natural).
  • Encierra escrituras multinivel en una transacción, especialmente cuando lees y luego escribes según lo leído.
  • Usa locks solo donde un cambio debe ganar (por ejemplo, una aprobación por pedido).
  • Registra señales de conflicto (errores de clave duplicada, fallos de serialización, timeouts de locks) y alerta si hay picos.

Si construyes flujos en Koder.ai, toma esto como criterios de aceptación: la app generada debe fallar de forma segura bajo reintentos y concurrencia, no solo pasar el camino feliz.

Escenario de ejemplo: evitar una doble aprobación

Dos empleados abren la misma solicitud de compra. Ambos hacen clic en Aprobar en cuestión de segundos. Ambas peticiones llegan al servidor.

Lo que puede salir mal es confuso: la solicitud queda "aprobada" dos veces, se envían dos notificaciones y cualquier total ligado a aprobaciones (presupuesto usado, contador diario) puede subir en 2. Ambas actualizaciones son válidas por sí mismas, pero colisionan.

Aquí hay un plan de corrección que funciona bien con una base de datos estilo PostgreSQL.

1) Haz la doble aprobación imposible en la base de datos

Añade una regla que garantice que solo pueda existir un registro de aprobación por solicitud. Por ejemplo, guarda aprobaciones en una tabla separada y aplica una restricción UNIQUE en request_id. Ahora el segundo insert falla aunque el código tenga un bug.

2) Encierra el cambio de estado en una transacción

Al aprobar, haz toda la transición en una sola transacción:

  • Bloquea la fila de la solicitud (o actualiza con una condición como WHERE status = 'pending').
  • Verifica que todavía esté pending.
  • Escribe el registro de aprobación.
  • Actualiza el estado de la solicitud a approved.

Si la segunda persona llega tarde, verá 0 filas actualizadas o un error de constraint único. En cualquier caso, solo un cambio gana.

3) Da feedback claro en la UI

Después del arreglo, la primera persona ve Aprobado y la confirmación normal. La segunda ve un mensaje amigable como: "Esta solicitud ya fue aprobada por otra persona. Actualiza para ver el estado más reciente." Sin cargas eternas, sin notificaciones duplicadas, sin fallos silenciosos.

Si generas un flujo CRUD en una plataforma como Koder.ai (backend en Go con PostgreSQL), puedes incorporar estas comprobaciones en la acción de aprobar una vez y reutilizar el patrón para otras acciones de "solo un ganador".

Siguientes pasos: convierte las correcciones de carrera en parte de tu rutina de build

Las condiciones de carrera son más fáciles de arreglar cuando las tratas como una rutina repetible, no como una búsqueda de bugs ocasional. Centra tu esfuerzo en las pocas rutas de escritura que importan y haz que sean aburridamente correctas antes de pulir cualquier otra cosa.

Empieza por nombrar tus principales puntos de colisión. En muchas apps CRUD es el mismo trío: contadores (likes, inventario, balances), cambios de estado (Draft -> Submitted -> Approved) y envíos dobles (doble clic, reintentos, redes lentas).

Una rutina que funciona:

  • Escribe la regla de verdad para cada flujo (por ejemplo: un pedido solo puede aprobarse una vez y solo desde pending).
  • Añade constraints en la base de datos primero (claves únicas, foreign keys, check constraints) para que la base de datos rechace estados imposibles.
  • Añade locks/transacciones solo donde un cambio debe ganar (por ejemplo, bloquear una fila durante la aprobación).
  • Añade una pequeña prueba de concurrencia por cada flujo crítico, orientada al fallo que temes (dos aprobaciones, dos incrementos, dos envíos de formulario).
  • Revisa el manejo de errores para que la UI muestre un mensaje claro cuando la base de datos rechace una escritura en conflicto.

Si construyes sobre Koder.ai, Planning Mode es un buen lugar para mapear cada flujo de escritura como pasos y reglas antes de generar cambios en Go y PostgreSQL. Los snapshots y rollback también son útiles cuando despliegas nuevas restricciones o comportamiento de locks y quieres una manera rápida de volver atrás si aparece un caso límite.

Con el tiempo, esto se convierte en hábito: cada nueva función de escritura tiene una constraint, un plan de transacción y una prueba de concurrencia. Así las condiciones de carrera en apps CRUD dejan de ser sorpresas.

Contenido
Cómo se ve una condición de carrera en una app CRUDPor qué las condiciones de carrera pasan más a menudo de lo que parecePuntos comunes de colisión a vigilarEnvios dobles y peticiones repetidas desde la interfazTransiciones de estado que se desincronizanContadores y totales que derivan con el tiempoPara de las colisiones con restricciones en la base de datos primeroUsa transacciones y locks cuando un cambio debe ganarPaso a paso: endurece una ruta de escritura de extremo a extremoErrores comunes que mantienen vivas las condiciones de carreraComprobaciones rápidas antes de lanzarEscenario de ejemplo: evitar una doble aprobaciónSiguientes pasos: convierte las correcciones de carrera en parte de tu rutina de build
Compartir
Koder.ai
Crea tu propia app con Koder hoy!

La mejor manera de entender el poder de Koder es verlo por ti mismo.

Empezar gratisReservar demo