Handle Concurrent Updates Without Race Conditions

Hi everyone,
I’m using PostgreSQL with n8n, and I’m trying to understand the best way to handle multiple workflows updating the same record at the same time.
For example, two webhook executions could try to update the same row simultaneously:UPDATE orders
SET status = ‘processed’
WHERE order_id = 1001;
My concern is avoiding issues like:
Lost updates
Race conditions
Duplicate processing
Inconsistent data
I’ve been reading about row-level locking (FOR UPDATE), optimistic locking, and transactions, but I’m not sure which approach works best in a production n8n environment.
For those running PostgreSQL with high-concurrency workflows:
• Do you rely on transactions alone, or do you also use row-level locks?
• When would you choose optimistic locking over pessimistic locking?
• Have you experienced deadlocks, and how did you handle them?
• Any production tips for keeping data consistent without hurting performance?
I’d love to hear what has worked well for others in real-world n8n deployments.

Describe the problem/error/question

What is the error message (if any)?

Please share your workflow

(Select the nodes on your canvas and use the keyboard shortcuts CMD+C/CTRL+C and CMD+V/CTRL+V to copy and paste the workflow.)

Share the output returned by the last node

Information on your n8n setup

  • n8n version:
  • Database (default: SQLite):
  • n8n EXECUTIONS_PROCESS setting (default: own, main):
  • Running n8n via (Docker, npm, n8n cloud, desktop app):
  • Operating system:

The best approach depends on how often the same records are updated, but for most high-concurrency workflows, transactions and row-level locking work well together.

Recommended approach

Use a transaction and lock the row before updating it:BEGIN;

SELECT *
FROM orders
WHERE order_id = 1001
FOR UPDATE;

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

COMMIT;

This prevents another transaction from modifying the same row until the current one finishes.

For high-volume systems
Keep transactions short
Index frequently queried columns
Use optimistic locking if update conflicts are rare

Hi @Keira_Becky
For a single status flip like your example, skip the explicit lock and let the UPDATE be the guard. One atomic statement that only touches the row if it hasn’t been processed yet:

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

The first execution updates the row, the second matches zero rows so RETURNING comes back empty, and you branch on “did I get a row back” to know if it was already handled. That kills lost updates and duplicate processing in one statement, no multi-step transaction needed.

If you do need a real read-modify-write with an explicit lock, it has to run inside a single Execute Query node, or turn on the node’s Transaction option. Separate Postgres nodes each open their own connection, so a lock taken in one node is released before the next node runs, and the lock does nothing.

For deadlocks under concurrent workers pulling a batch, use SKIP LOCKED so each worker grabs different rows instead of blocking on the same one:

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

Then set Retry On Fail on the node so a transient serialization error just retries.

Hey @Keira_Becky

In any n8n workflow that touches PostgreSQL, always wrap the database actions in a single transaction. In n8n this is done with a “Start Transaction” node, the required SELECT/UPDATE statements, and a “Commit Transaction” (or Rollback on error). Transactions guarantee that a failure aborts the whole set of changes, preventing partial updates and keeping the data consistent even if a workflow crashes.

Pessimistic locking (SELECT … FOR UPDATE or FOR UPDATE SKIP LOCKED) is ideal when you need exactly‑once processing, when many workers contend for the same few rows, or when a piece of logic touches multiple rows that must stay in sync. The lock is held until the transaction commits, ensuring that no other workflow can read or modify the locked row, which eliminates lost updates and duplicate processing.

Optimistic locking works best under low contention. By adding a version (or updated_at) column and updating with a condition like WHERE version = $oldVersion, you let concurrent workers attempt the update; only the first succeeds, and the others detect a conflict (zero rows affected) and can retry. This approach avoids the overhead of locks and is useful for batch updates or stateless webhook executions where a simple retry loop suffices.

Even with careful locking, deadlocks can arise when workflows lock rows in different orders. Mitigate them by always acquiring locks in a deterministic order (e.g., ORDER BY order_id ASC), using SKIP LOCKED for queue‑style processing, and implementing retry logic for the 40P01 deadlock error. Monitoring settings such as log_lock_waits = on help surface deadlock incidents quickly.

To keep performance high, keep transactions short, avoid external HTTP calls inside a transaction, and ensure relevant columns (order_id, status, version) are indexed. If many rows need the same change, batch them in a single UPDATE statement rather than spawning a separate workflow per row. Using a limited worker pool and SKIP LOCKED reduces lock contention and prevents workers from idling while waiting for a lock.

Use optimistic locking when contention is rare and you can tolerate retries; switch to pessimistic locking when you must guarantee single‑threaded access or when multiple rows are involved in a business rule. For queue‑style processing, FOR UPDATE SKIP LOCKED combined with a short retry/back‑off loop is the most common production pattern in n8n. Following these guidelines yields data‑consistent, high‑throughput workflows without sacrificing performance.

Thanks a lot @Niffzy @Anshul_Namdev @kjooleng For the detailed explanation

The atomic UPDATE … RETURNING approach makes sense for simple status transitions, while transactions and locking patterns become important when multiple related changes need to happen together. This clarified when to use each approach in n8n production workflows. Appreciate the insights