Solving Race Conditions in n8n: A Complete Guide to Distributed Locking
8kit Team•
If you've ever had two n8n workflow executions fight over the same resource, updating the same CRM record, processing the same webhook twice, or writing conflicting data to your database, you've hit a race condition. And you're not alone.
Race conditions are one of the most discussed reliability problems in the n8n community, with 5+ active threads and over 13,000 combined views on community.n8n.io. The problem gets worse as your workflows scale: more executions running concurrently means more chances for collisions.
This guide covers what causes race conditions in n8n, the common workarounds people try (and why they break), and how to implement proper distributed locking with 8kit's Exclusivity pattern.
What Are Race Conditions in n8n?
A race condition occurs when two or more workflow executions access and modify shared state simultaneously, and the final result depends on which execution "wins the race."
Common scenarios in n8n:
1. Webhook-triggered concurrent updates
Your Shopify store fires two order update webhooks within milliseconds. Both n8n executions read the current inventory count, subtract 1, and write back. If inventory was 10, you'd expect 8, but both read 10, both write 9. You just lost a unit.
2. Scheduled workflow overlap
A cron-triggered sync workflow takes 15 minutes to run, but it's scheduled every 10 minutes. The second execution starts while the first is still running. Both try to update the same records in your ERP.
3. Retry collisions
A workflow fails partway through and retries. The retry runs while manual cleanup is happening, or while another webhook triggers the same logic.
4. Parallel branch conflicts
Within a single workflow, parallel branches that converge on the same external API can create conflicts if that API isn't idempotent.
The DIY Approaches (and Why They Break)
Approach 1: Disable concurrent executions
n8n lets you toggle "Execute workflow concurrently" off. Problem solved?
Not really. This creates a queue, executions wait their turn. For webhook-triggered workflows, this means:
- Incoming webhooks can timeout while waiting
- Processing latency spikes unpredictably
- You've traded a data integrity problem for a throughput problem
Worse, this is a per-workflow setting. If you have 5 workflows that all touch the same CRM record, disabling concurrency on each one doesn't prevent cross-workflow conflicts.
Approach 2: Redis-based locking
Some advanced users set up external Redis with the Function node:
// In a Function node, acquire lock
const redis = require('redis');
const client = redis.createClient({ url: process.env.REDIS_URL });
await client.connect();
const lockKey = `lock:order:${items[0].json.orderId}`;
const acquired = await client.set(lockKey, 'locked', { NX: true, EX: 30 });
if (!acquired) {
throw new Error('Could not acquire lock, another execution is processing this order');
}
return items;
This works in principle, but in practice:
- No automatic cleanup, if your workflow crashes between acquire and release, the lock stays until TTL expires. Meanwhile, legitimate executions are blocked.
- No visibility, you can't see which locks are held, by which execution, or for how long without SSHing into Redis.
- Manual release, you need a matching Function node at every exit point of your workflow (success, error, timeout). Miss one and you have a stuck lock.
- Operational burden, you're now running Redis infrastructure for what should be a workflow concern.
Approach 3: Database flags
Some users add a "processing" boolean column to their database and check it before proceeding:
UPDATE orders SET processing = true WHERE id = 123 AND processing = false;
If the update affects 0 rows, the order is already being processed. This is actually a decent pattern in theory, but:
- You need direct database access from n8n (not always available)
- You're coupling workflow logic to database schema
- Cleanup on failure requires the same careful handling as Redis locks
- It doesn't generalize across systems (works for your DB, not for API calls)
The 8kit Approach: Exclusivity (Distributed Locks)
8kit's Exclusivity pattern gives you distributed locking as a drag-and-drop n8n node. No Redis, no custom code, no database flags.
How it works
- Acquire a lock on a named resource (e.g.,
order:12345) - Do your work, update the CRM, process the order, sync the data
- Release the lock when done
If another execution tries to acquire the same lock, it either waits (with configurable timeout) or fails immediately, your choice.
Setting it up
Drop the 8kit Exclusivity node into your workflow:
Lock Acquire node configuration:
- Operation: Acquire
- Lock Name: Use an expression like
order:{{ $json.orderId }}for per-resource locking, orsync:shopify-erpfor workflow-level locking - Timeout: How long to wait for the lock (0 = fail immediately if locked)
- TTL: Maximum lock duration, automatic safety net if your workflow crashes
Lock Release node configuration:
- Operation: Release
- Lock Name: Same name used in Acquire
What makes this better than DIY
| Feature | DIY Redis | Database Flag | 8kit Exclusivity |
|---|---|---|---|
| Setup time | Hours (infra + code) | Hours (schema + code) | Minutes (install node) |
| Automatic crash cleanup | No (wait for TTL) | No (manual) | Yes (TTL + server-side tracking) |
| Visibility / dashboard | No | No | Yes, see all active locks |
| Cross-workflow locking | Manual coordination | Not practical | Built-in (same lock name) |
| Error handling | Manual at every exit | Manual at every exit | Automatic release on failure |
| Operational overhead | Redis infrastructure | Database schema changes | 8kit server (self-hosted or cloud) |
Real-world example: Shopify order sync
Here's how a production Shopify-to-ERP sync workflow looks with 8kit:
Webhook (Shopify order update)
→ 8kit Exclusivity: Acquire lock "order:{{ $json.id }}"
→ Fetch current ERP record
→ Compare and merge changes
→ Update ERP
→ 8kit Exclusivity: Release lock "order:{{ $json.id }}"
If two webhooks fire for the same order simultaneously:
- Execution A acquires the lock and proceeds
- Execution B waits (or fails gracefully, depending on your config)
- Execution A finishes and releases the lock
- Execution B acquires the lock and processes with the updated state
No data corruption. No lost updates. No Redis to maintain.
Advanced Patterns
Per-resource vs. workflow-level locks
- Per-resource: Lock on a specific entity (order ID, customer ID). Allows concurrent processing of different entities while protecting same entity access. Use:
lock:order:{{ $json.orderId }} - Workflow-level: Lock on the workflow itself. Only one execution runs at a time, but without the latency issues of disabling concurrency (because you control timeout behavior). Use:
lock:workflow:shopify-sync
Lock hierarchies
For complex workflows that touch multiple resources:
Acquire lock "customer:{{ $json.customerId }}"
→ Acquire lock "order:{{ $json.orderId }}"
→ Do work
→ Release "order:{{ $json.orderId }}"
→ Release "customer:{{ $json.customerId }}"
Always acquire in a consistent order to prevent deadlocks.
Combining with other 8kit patterns
Exclusivity works best alongside:
- Do-once (Uniqs): Dedup first, then lock. No point locking for a duplicate.
- Temporal (Last Updated): After releasing the lock, update the timestamp so incremental sync knows this resource was just processed.
Getting Started
- Install the 8kit community node:
npm install n8n-nodes-8kit(or search "8kit" in the n8n community nodes panel) - Connect to your 8kit server (self-hosted or cloud)
- Add the Exclusivity node to your workflow
- Set your lock name using expressions for dynamic, per-resource locking
Your workflows just got bulletproof.
8kit provides four enterprise automation patterns for n8n: deduplication, cross-system mapping, distributed locking, and change tracking. Learn more at 8kit.io