Race conditions in n8n: stopping a workflow from running twice on itself
Published 1 September 2026 · 6 min read
A customer confirms a booking through a form. At almost the same instant, down to the millisecond, a second customer confirms the same booking from another tab they'd left open. Both webhooks land on the same n8n instance a few milliseconds apart, each triggers a run of the same workflow, and both read "slot available" before either one has had time to write "slot taken." Result: two confirmations sent for one spot. This isn't a bug in the workflow — it's a race condition, a structural problem that affects n8n exactly like any system that processes requests in parallel.
What n8n's concurrency limit actually does
n8n exposes a concurrency setting through the N8N_CONCURRENCY_PRODUCTION_LIMIT environment variable, available in both regular mode and queue mode. Set to -1 (unlimited) by default, it can be set to a specific integer: beyond that cap, new production executions are queued FIFO instead of starting immediately. Three details change what you can actually expect from it:
- It applies only to production executions — those started by a webhook or a trigger. Manual executions, sub-workflows, and error executions aren't counted.
- It caps the entire instance, across every workflow — not one workflow in particular. A cap of 10 protects the server from an overall overload, but does nothing to stop two triggers of the same workflow from running in parallel if the instance is otherwise lightly loaded.
- It knows nothing about the business resource the workflow manipulates internally. Two executions perfectly allowed by the concurrency limit can very well write, at the same time, to the same stock row or the same calendar slot.
In other words: N8N_CONCURRENCY_PRODUCTION_LIMIT is a traffic regulator for the instance, not a lock for a given workflow. n8n doesn't currently offer a native setting along the lines of "only ever allow one run of this specific workflow at a time" — the workflow itself has to protect against that.
Why checking before writing isn't enough
The natural instinct is to add an IF node that checks the resource's state before changing it — "is this slot already taken?" The problem is that reading and writing aren't a single atomic operation in a typical n8n workflow: some time, however brief, passes between the node that reads the state and the one that writes the new one. If a second execution reads the state exactly within that window, it still sees the old value and makes the same decision. The more HTTP calls a workflow makes between the read and the write (enrichment, an LLM call, a confirmation email), the wider that window gets and the more likely the collision becomes — this is exactly the mechanism behind the calendar booking conflicts we documented recently, except here both writes come from the same workflow rather than from two different tools.
A 2025 study by cybersecurity researchers at the University of Cagliari, published in Computers & Security, precisely measured the factors that make a race condition exploitable in a web application — the gap between read and write, network latency, the HTTP protocol used — and shows that the window needed to trigger a collision can be as short as a few milliseconds, well within reach of two requests sent moments apart (Loi, Pisu, Regano, Maiorca & Giacinto, 2025, Race Against Time: Investigating the Factors that Influence Web Race Condition Exploits, Computers & Security). The study's context is offensive security, but the mechanism it quantifies is identical to the one that produces a double-booking or an oversold item in an automation workflow: it isn't the workflow's speed that protects you, it's the atomicity of the critical operation.
Building a lock with a Data Table
The fix that solves the problem at its root is an application-level lock: before touching a resource, the workflow tries to acquire a lock on it; if it succeeds, it processes the resource and releases the lock; if not, it waits or backs off. n8n's Data Tables are a natural fit for this, with no external database to provision:
- A
lockstable with two columns:resource_id(the key — say, the slot or item identifier) andlocked_at(the timestamp when the lock was acquired). - At the start of the workflow, a Data Table node tries to insert a row for that
resource_id. A uniqueness constraint on the column makes the insert fail if a row already exists — it's that failure, not a prior read, that acts as the test: the insert operation itself is atomic on the database side, unlike a read-then-write pair. - If the insert succeeds, the workflow holds the lock: it processes the booking or order, then deletes the row from the table once done (or from the Error Workflow if something fails, so a lock is never left orphaned).
- If the insert fails, another execution already holds the lock: a Wait node pauses for a few seconds and retries, or the workflow stops cleanly and reports the conflict instead of processing a resource that's already taken.
One detail not to skip: always add an expiry to the lock (for instance, ignore any row where locked_at is older than two minutes) so that an execution which crashes before the release step doesn't block the resource for everyone indefinitely.
What Dijkstra had already solved in 1965
There's nothing new about the principle: it's exactly the mutual exclusion problem formalized by Edsger Dijkstra in a foundational computer science paper, Solution of a Problem in Concurrent Programming Control, Communications of the ACM, 1965, which lays out what any concurrent system must guarantee: that only one execution at a time enters a critical section, regardless of arrival order or the relative speed of the processes involved. An n8n workflow that modifies a shared resource is, quite simply, a concurrent process — the Data Table just plays the role of the lock Dijkstra described for system processes, sixty years before no-code arrived.
Concrete use cases
- E-commerce stock decrement. Two orders for the last unit of an item, arriving through two channels (website and marketplace), processed by the same n8n workflow: without a lock, both can read "1 in stock" and confirm the sale twice.
- Retried Stripe webhooks. A Stripe webhook that fails over HTTP can be automatically resent by Stripe — a lock on the invoice ID prevents generating the same document twice, on top of the idempotency already recommended for webhooks.
- Sequential document generation. A workflow that assigns an incremental invoice or quote number from a counter must lock that counter for the duration of incrementing and saving it, or risk assigning the same number to two different documents.
Pitfalls to avoid
- Confusing the concurrency limit with a business lock:
N8N_CONCURRENCY_PRODUCTION_LIMITprotects the instance, not a specific resource. - Checking then writing as two separate steps instead of relying on an atomic operation (an insert with a uniqueness constraint) to acquire the lock.
- Forgetting to expire the lock: an execution that crashes before releasing it blocks the resource indefinitely for every run after it.
- Locking too broad a resource ("the whole stock" instead of "this specific item") — that eliminates the race condition but needlessly serializes operations that would never have conflicted with each other.
In short
N8N_CONCURRENCY_PRODUCTION_LIMIT regulates traffic on an n8n instance, but it doesn't protect any specific business resource against two simultaneous runs of the same workflow. The only reliable protection is an application-level lock, built with a Data Table and a uniqueness constraint that makes acquiring the lock atomic — a sixty-year-old principle, applied with today's no-code tools. The order-processing and invoicing workflows in the Compliance & Audit Pack (€149) follow this same locking logic on sensitive sequential operations; if your instance already combines several of these high-stakes automations, the Complete FlowKit Bundle (€269 instead of €347) brings every pack together on a shared foundation.
FAQ
Frequently asked questions
Does N8N_CONCURRENCY_PRODUCTION_LIMIT prevent two simultaneous executions of the same workflow?
Not directly. This variable caps the total number of active production executions across the whole instance, all workflows combined — it slows overall throughput but doesn't stop two triggers of the same workflow, on the same resource, from running in parallel if the cap hasn't been reached. It's a traffic throttle, not a business-level lock.
Does n8n's queue mode solve the concurrency problem?
Queue mode spreads executions across multiple workers and lets you set N8N_CONCURRENCY_PRODUCTION_LIMIT per worker, which helps absorb high load without overwhelming the instance. But the variable stays a global cap: it still doesn't guarantee that a specific resource (a time slot, a stock line, an invoice) is only ever touched by one execution at a time.
Why not just check the resource's state before changing it?
Because checking and writing aren't a single atomic operation in a typical n8n workflow: between the node that reads the state ("this slot is free") and the node that writes the new state, another execution can read that exact same state before the first one has written its update. That gap is precisely what defines a race condition, no matter how fast the workflow runs.
Bundle FlowKit Complet
€269