Hi GUYS I’m using n8n with Microsoft SQL Server, and multiple workflow executions can update the same customer record at almost the same time.
I’m worried about race conditions and lost updates when two executions read the same record and then try to update it.
UPDATE Customers
SET
Status = @status,
UpdatedAt = GETUTCDATE()
WHERE CustomerId = @customerId;
Would you recommend transactions, UPDLOCK/ROWLOCK, optimistic concurrency using a rowversion column, or a combination? Also, how would you structure the n8n workflow so a failed or concurrent update can be safely retried without overwriting newer data?
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.)
Use optimistic concurrency with SQL Server’s rowversion, especially when multiple n8n executions can update the same record.
Add a rowversion column:ALTER TABLE Customers
ADD RowVersion ROWVERSION;
When n8n reads the customer, keep the current RowVersion. Then only update the record if that version is still unchanged: UPDATE Customers
SET
Status = @status,
UpdatedAt = SYSUTCDATETIME()
WHERE CustomerId = @customerId
AND RowVersion = @rowVersion;
If the update affects 0 rows, another workflow has already changed the record. n8n can then re-read the latest version and decide whether to retry or stop.
I’d use transactions when multiple SQL operations need to succeed together, but I wouldn’t add locks everywhere.
If two n8n executions read the same RowVersion and both try to update the customer, how would you handle the execution that gets 0 rows updated and Would you retry automatically with the latest RowVersion, or should n8n stop and flag it as a concurrency conflict to avoid accidentally overwriting the other workflow’s changes?
I wouldn’t automatically overwrite the record. If the update returns 0 rows affected, it usually means another n8n execution changed the record first.
I’d handle it like this:UPDATE
↓
0 rows affected?
↓
YES → Re-read latest record
↓
Compare changes
↓
Retry only if safe
First, read the latest version:SELECT
CustomerId,
Status,
RowVersion
FROM Customers
WHERE CustomerId = @customerId;
Then perform a version protected update:UPDATE Customers
SET
Status = @status,
UpdatedAt = SYSUTCDATETIME()
WHERE CustomerId = @customerId
AND RowVersion = @rowVersion;
check the affected-row count. If it’s 0, don’t retry the same update blindly. Fetch the latest record, compare the changes, and only retry with the new RowVersion if the update is still safe.
Hi Mary — @Niffzy’s rowversion suggestion is the default I’d choose too. One small but important distinction: a transaction by itself doesn’t prevent a lost update, and ROWLOCK is only a hint, so I wouldn’t use either one as the main correctness mechanism.
I’d make the update return the new version:
UPDATE dbo.Customers
SET
Status = @status,
UpdatedAt = SYSUTCDATETIME()
OUTPUT inserted.RowVersion
WHERE CustomerId = @customerId
AND RowVersion = @expectedRowVersion;
In n8n, I’d treat an empty result / zero affected rows as a concurrency conflict:
Read the latest row again.
Re-evaluate whether the requested status change is still valid.
Retry only with the new RowVersion, using a small maximum attempt count and jitter.
If the operation also calls an external API, make that side effect idempotent before retrying the database step.
One extra nuance: rowversion tells you that the database row changed after you read it, but it doesn’t tell you whether an incoming webhook event is newer in business terms. If the source provides a monotonic event version or sequence number, store that separately and add something like:
AND SourceVersion < @incomingSourceVersion
I’d also keep processed webhook EventId values behind a unique constraint. rowversion protects against concurrent writers; the event ID protects against duplicate delivery. They’re related, but sadly distributed systems like collecting both kinds of trouble
I’d reserve UPDLOCK/HOLDLOCK for a short transaction containing several dependent statements that genuinely need pessimistic locking. For a single-row update, optimistic concurrency is usually simpler and kinder to throughput.
Hi @Mary_Berry
A version-guarded UPDATE that matches nothing is still a successful query, so the Microsoft SQL node reports success and n8n’s “Retry On Fail” and “On Error” settings never engage on a conflict. “Retry On Fail” is the wrong tool here anyway, since it reruns the same node with the same stale expected version. Structure it as an explicit loop instead: an IF node on the returned row count, the conflict branch wired back to the re-read node, and a counter in the loop to cap the attempts.
One detail for that node, its Query Parameters are positional and referenced as $1, $2, $3 in order, not as named @variables.