Gestionar actualizaciones concurrentes sin condiciones de carrera

Hola a todos,
Estoy usando PostgreSQL con n8n e intento entender la mejor manera de manejar múltiples flujos de trabajo que actualicen el mismo registro simultáneamente.
Por ejemplo, dos ejecuciones de webhook podrían intentar actualizar la misma fila al mismo tiempo:
UPDATE orders
SET status = ‘processed’
WHERE order_id = 1001;
Mi preocupación es evitar problemas como:
Actualizaciones perdidas
Condiciones de carrera
Procesamiento duplicado
Datos inconsistentes
He estado leyendo sobre bloqueos a nivel de fila (FOR UPDATE), bloqueo optimista y transacciones, pero no estoy seguro de qué enfoque funciona mejor en un entorno n8n de producción.
Para quienes ejecutan PostgreSQL con flujos de trabajo de alta concurrencia:
• ¿Confían solo en transacciones, o también utilizan bloqueos a nivel de fila?
• ¿Cuándo elegirías bloqueo optimista en lugar de bloqueo pesimista?
• ¿Has experimentado bloqueos mutuos y cómo los manejaste?
• ¿Algún consejo de producción para mantener los datos consistentes sin afectar el rendimiento?
Me encantaría escuchar qué ha funcionado bien para otros en implementaciones n8n del mundo real.

Describe el problema/error/pregunta

¿Cuál es el mensaje de error (si hay alguno)?

Por favor, comparte tu flujo de trabajo

(Selecciona los nodos en tu lienzo y utiliza los atajos de teclado CMD+C/CTRL+C y CMD+V/CTRL+V para copiar y pegar el flujo de trabajo.)

Comparte el resultado devuelto por el último nodo

Información sobre tu configuración de n8n

  • Versión de n8n:
  • Base de datos (predeterminada: SQLite):
  • Configuración de n8n EXECUTIONS_PROCESS (predeterminada: own, main):
  • Ejecutando n8n mediante (Docker, npm, n8n cloud, aplicación de escritorio):
  • Sistema operativo:

El mejor enfoque depende de la frecuencia con la que se actualicen los mismos registros, pero para la mayoría de los flujos de trabajo de alta concurrencia, las transacciones y el bloqueo a nivel de fila funcionan bien juntos.

Enfoque recomendado

Usa una transacción y bloquea la fila antes de actualizarla:
BEGIN;

SELECT *
FROM orders
WHERE order_id = 1001
FOR UPDATE;

UPDATE orders
SET status = ‘processed’
WHERE order_id = 1001;

COMMIT;

Esto evita que otra transacción modifique la misma fila hasta que se complete la actual.

Para sistemas de alto volumen
Mantén las transacciones cortas
Indexa las columnas consultadas frecuentemente
Usa bloqueo optimista si los conflictos de actualización son raros

Hola @Keira_Becky
Para un cambio de estado simple como tu ejemplo, omite el bloqueo explícito y deja que UPDATE sea la protección. Una sentencia atómica única que solo toca la fila si aún no ha sido procesada:

UPDATE orders
SET status = 'processed'
WHERE order_id = 1001 AND status <> 'processed'
RETURNING order_id;

La primera ejecución actualiza la fila, la segunda no coincide con ninguna fila por lo que RETURNING vuelve vacío, y ramificas en «¿obtuve una fila?» para saber si ya fue manejado. Eso elimina actualizaciones perdidas y procesamiento duplicado en una sentencia, sin necesidad de transacción multisecuencia.

Si realmente necesitas un read-modify-write con bloqueo explícito, debe ejecutarse dentro de un único nodo Execute Query, o activa la opción Transaction del nodo. Los nodos Postgres separados abren cada uno su propia conexión, por lo que un bloqueo tomado en un nodo se libera antes de que se ejecute el siguiente nodo, y el bloqueo no hace nada.

Para deadlocks bajo workers concurrentes que extraen un lote, usa SKIP LOCKED para que cada worker tome diferentes filas en lugar de bloquearse en la misma:

SELECT order_id
FROM orders
WHERE status = 'pending'
FOR UPDATE SKIP LOCKED
LIMIT 100;

Luego establece Retry On Fail en el nodo para que un error transiente de serialización simplemente reintente.

Hola @Keira_Becky

En cualquier flujo de trabajo de n8n que toque PostgreSQL, siempre envuelve las acciones de base de datos en una única transacción. En n8n esto se hace con un nodo «Start Transaction», las sentencias SELECT/UPDATE requeridas y un nodo «Commit Transaction» (o Rollback en caso de error). Las transacciones garantizan que un fallo cancele todo el conjunto de cambios, evitando actualizaciones parciales y manteniendo la consistencia de los datos incluso si un flujo de trabajo se bloquea.

El bloqueo pesimista (SELECT … FOR UPDATE o FOR UPDATE SKIP LOCKED) es ideal cuando necesitas procesamiento exactamente una vez, cuando muchos workers compiten por las mismas pocas filas, o cuando una lógica toca múltiples filas que deben mantenerse sincronizadas. El bloqueo se mantiene hasta que se confirma la transacción, asegurando que ningún otro flujo de trabajo pueda leer o modificar la fila bloqueada, lo que elimina las actualizaciones perdidas y el procesamiento duplicado.

El bloqueo optimista funciona mejor bajo poca contención. Al agregar una columna version (o updated_at) y actualizar con una condición como WHERE version = $oldVersion, permites que workers concurrentes intenten la actualización; solo el primero tiene éxito y los otros detectan un conflicto (cero filas afectadas) y pueden reintentar. Este enfoque evita la sobrecarga de los bloqueos y es útil para actualizaciones en lote o ejecuciones de webhook sin estado donde un simple bucle de reintento es suficiente.

Incluso con bloqueos cuidadosos, pueden surgir interbloqueos cuando los flujos de trabajo bloquean filas en diferentes órdenes. Mitígalos siempre adquiriendo bloqueos en un orden determinista (por ejemplo, ORDER BY order_id ASC), usando SKIP LOCKED para procesamiento de tipo cola, e implementando lógica de reintento para el error de interbloqueo 40P01. La supervisión de configuraciones como log_lock_waits = on ayuda a detectar rápidamente incidentes de interbloqueo.

Para mantener el rendimiento alto, mantén las transacciones cortas, evita llamadas HTTP externas dentro de una transacción, y asegúrate de que las columnas relevantes (order_id, status, version) estén indexadas. Si muchas filas necesitan el mismo cambio, agrúpalas en una única sentencia UPDATE en lugar de generar un flujo de trabajo separado por fila. Usar un grupo de workers limitado y SKIP LOCKED reduce la contención de bloqueos y evita que los workers se queden inactivos esperando un bloqueo.

Usa bloqueo optimista cuando la contención es rara y puedes tolerar reintentos; cambia a bloqueo pesimista cuando debes garantizar acceso de un único hilo o cuando múltiples filas están involucradas en una regla de negocio. Para procesamiento de tipo cola, FOR UPDATE SKIP LOCKED combinado con un bucle corto de reintento/retroceso es el patrón de producción más común en n8n. Seguir estas directrices produce flujos de trabajo consistentes en datos y de alto rendimiento sin sacrificar el rendimiento.

Muchas gracias @Niffzy @Anshul_Namdev @kjooleng por la explicación detallada

El enfoque atómico UPDATE … RETURNING tiene sentido para transiciones de estado simples, mientras que las transacciones y patrones de bloqueo se vuelven importantes cuando múltiples cambios relacionados necesitan ocurrir juntos. Esto aclaró cuándo usar cada enfoque en flujos de trabajo de producción de n8n. Agradezco los conocimientos