At a Glance — Race Conditions in n8n Agent Workflows

  • The pattern that breaks: check shared state (a calendar, stock count, CRM record), then act on it — with nothing stopping a second execution from checking in between
  • Why AI makes it worse: an agent that reasons for several seconds between "check" and "act" stretches the danger window from milliseconds to seconds
  • Why it's not rare: n8n runs each webhook call as its own execution, webhook senders resend, Retry On Fail re-runs nodes, and agents sometimes call the same tool twice
  • The fix: claim before you act (in a store that refuses a second claim), give every business action an idempotency key, and re-check the real system right before the final write. Then handle failure: release or expire claims that never reach a booking, and mark a key done only after the API confirms
  • What doesn't fix it: a better prompt, a stricter model, or n8n queue mode — queue mode adds parallelism, so it makes races more likely

Why this keeps happening

A popular n8n appointment-booking workflow shared on LinkedIn this month drew the same comment from at least seven different engineers: what happens when two people ask for the same slot at once? The answer, in almost every check-then-act workflow, is that both get it.

n8n runs every incoming request as its own execution, and executions run in parallel. Execution A reads the calendar: 3pm free. Execution B reads the calendar: 3pm free. A books 3pm. B books 3pm. Each step did exactly what it was built to do — the failure lives in the gap between them, which is why it never shows up in testing, where requests arrive one at a time.

Why the AI step makes it worse

In a plain workflow, check and act might sit a few hundred milliseconds apart. Put an AI Agent node between them — reading the request, deciding which slot fits, calling a tool, maybe confirming with the user — and that gap grows to several seconds, sometimes much longer when there's a conversation in between. The probability of a collision grows with that window. An agent that works flawlessly in a demo can double-book within its first busy hour.

Agents also add their own duplicates: a model may call the same booking tool twice in one run, or a timeout makes the workflow retry a call that actually succeeded. Neither is the model "making a mistake" in a way a better prompt fixes — they're ordinary distributed-systems problems arriving through a new door.

Where duplicate and parallel runs come from

The fix: claim first, key every action, re-check before writing

1. Claim the resource before acting on it

Instead of "check, then book", the workflow first tries to claim the slot in a store that refuses a second claim. Only the execution that wins the claim continues. In Postgres that's a single insert against a unique constraint:

INSERT INTO slot_claims (slot_id, execution_id)
VALUES ('2026-10-08T15:00', '{{ $execution.id }}')
ON CONFLICT (slot_id) DO NOTHING
RETURNING slot_id;

If a row comes back, this execution owns the slot. If nothing comes back, someone else got there first — route to a clean "that time was just taken" response. A Redis key set only if it doesn't already exist does the same job. What matters is that the database or Redis makes the decision atomically, not an IF node comparing two values that may already be stale.

2. Give every business action an idempotency key

Build one ID per real-world action — the booking request ID, the order ID, the webhook event ID — record it before the side effect, and check it first on every attempt. When the webhook resends, the retry fires, or the agent calls the tool again, the second attempt sees the key is already done and stops instead of booking or charging twice. Where the downstream API accepts an idempotency key of its own (Stripe does), pass it through as well.

3. Re-check the real system right before the final write

Your claim table protects against other runs of this workflow. It doesn't know about someone booking the same slot directly in the calendar app. A last read immediately before the write — and a clear failure path if the slot is gone — covers what the claim can't.

Let the agent decide, not commit

The cleanest structure keeps the AI out of the critical section entirely: the agent figures out what the user wants, then hands a structured request to a plain, deterministic sub-workflow that does claim → re-check → book. The slow, unpredictable part stays outside the window where races happen.

When the booking fails: release the claim, confirm the key

Added October 15, 2026, after a reader pointed out that the fix above leaves two failure paths open.

The claim and the idempotency key both prevent duplicates, but each can leave a stuck record if the booking call fails or times out after them. Close both.

Give the claim a status and a clock

If an execution wins the claim and the calendar call then fails, the plain insert above leaves the row behind and the slot is blocked for everyone until someone deletes it by hand. Add a status and a timestamp, and let a lost claim be taken again:

CREATE TABLE slot_claims (
  slot_id      text PRIMARY KEY,
  execution_id text NOT NULL,
  status       text NOT NULL DEFAULT 'claimed',  -- claimed, booked, released
  claimed_at   timestamptz NOT NULL DEFAULT now()
);

-- claim: succeeds on a new slot, or on a slot that was released
INSERT INTO slot_claims (slot_id, execution_id)
VALUES ('2026-10-08T15:00', '{{ $execution.id }}')
ON CONFLICT (slot_id) DO UPDATE
  SET execution_id = EXCLUDED.execution_id, status = 'claimed', claimed_at = now()
  WHERE slot_claims.status = 'released'
RETURNING slot_id;

Then use the status on every path. When the booking succeeds, set it to booked. When it fails, the error path sets it to released so the slot comes back. For claims that never reached either (the workflow crashed, or the call hung), run a scheduled cleanup that frees claims still claimed after longer than a booking should take. One caution: a timeout does not prove the booking failed, so check the real calendar before releasing, or you will free a slot that was in fact booked.

Record the key as pending, mark it done after confirmation

The mirror problem: if the key is recorded before the side effect and the side effect fails, the retry sees the key and stops, and the booking is lost without a trace. Record the key as pending first and mark it done only after the API confirms. A retry then behaves by what it finds:

How to test for it before customers do

Fire the same request at the webhook twenty times at once — a short script or a load-testing tool does it in seconds — and count the bookings. One is the right answer. Then resend a single webhook event three times and check that nothing happens twice. Then test the failure path: kill the booking call halfway (a timeout or an error injected after the claim) and check that the slot comes back and that a retry neither double-books nor loses the booking. All three tests take minutes and catch failures that ordinary click-through testing never will.

Frequently asked questions

Why does my n8n booking workflow double-book? Because n8n runs each incoming request as its own execution, and two executions can run at the same time. If both check the calendar before either has booked, both see the slot as free and both book it. Nothing in a check-then-act workflow stops the second one — it's a race condition, not a bug in a node.

Why are AI agent workflows more prone to this? The window between checking and acting is the danger zone, and an AI step widens it. A plain workflow might check and book within a few hundred milliseconds; an agent that checks availability, reasons for several seconds, then calls a booking tool leaves a gap many times longer — plenty of time for a second request to land in the middle.

How do you prevent double-booking in n8n? Claim the resource before acting on it, in a store that refuses a second claim — for example a database insert against a unique constraint on the slot, or a Redis key that can only be set if it doesn't already exist. Only the execution that wins the claim goes on to book; the other gets a clean 'slot taken' path. Then re-check the real system right before the final write.

What is an idempotency key and why does n8n need one? A unique ID for one business action — one booking, one payment — that the workflow records before acting and checks before acting again. Webhook senders resend, n8n's Retry On Fail setting re-runs nodes, and agents sometimes call the same tool twice. With a key, the second attempt sees the action already happened and stops, instead of booking or charging twice.

Does n8n queue mode fix race conditions? No — queue mode spreads executions across workers so more of them run in parallel, which makes races more likely, not less. Concurrency has to be handled in the workflow's design: claim first, use idempotency keys, re-check before writing.

Need This Built?

Entech Solutions builds n8n agent workflows that hold up under real traffic — claim-first concurrency, idempotent side effects, and load tests before launch. See our n8n AI & Agentic Workflow Design page, our guide to n8n error handling, or get in touch to review a workflow you already run.