A scheduled n8n workflow posts twice. Or writes two CRM rows. Or pays for the same API call twice.
The tempting fix is to add a Remove Duplicates node and move on. That can hide one symptom while leaving the actual failure untouched.
Before changing the workflow, answer one question:
Did n8n create two executions, did one execution create two items, or did one logical action get replayed after a partial success?
Those are three different incidents. They need three different fixes.
If you want a broader pre-production check first, run Builderlog's private 60-second n8n reliability audit. It runs in your browser, uploads nothing, and asks for no email.
First: capture a receipt before you touch anything
Open the execution list and record these fields for the duplicated action:
| Field | What to capture |
|---|---|
| Execution IDs | One ID or two different IDs? |
| Started timestamps | Same second, seconds apart, or a manual retry later? |
| Trigger output count | How many items left the Schedule Trigger? |
| Pre-mutation input count | How many items reached the costly write/send node? |
| Business key | Order ID, lead ID, conversation ID, or another stable source key |
| Mutation receipt | Was the external write already accepted before the run failed? |
Do not delete the workflow or execution history yet. n8n's execution view is the evidence that separates the three branches.
Branch 1: two execution IDs at nearly the same time
If you see two independent execution IDs, the workflow did not fan out inside one run. Something registered or emitted the trigger twice.
Check these in order:
- Two active workflow copies. Search by name, tag, and trigger interval. A staging copy and a production copy can both be published.
- Two self-hosted main processes. Inspect the running containers and deployment definitions. A second Compose project or an old main process can register the same timer.
-
A multi-main deployment without the intended leader setup. In n8n queue mode, timer and poller work belongs to the leader. The official queue-mode documentation says every main must share queue mode, Postgres, Redis, the same n8n version, and
N8N_MULTI_MAIN_SETUP_ENABLED=truefor the supported multi-main configuration. - A schedule change that was never republished. n8n's Schedule Trigger troubleshooting guide notes that interval and variable changes take effect when the workflow is published again.
The acceptance test is not “it ran once after I restarted.” Require several scheduled windows with exactly one execution receipt per expected slot.
An idempotency guard is still useful, but it is the safety belt—not the diagnosis. If two mains are scheduling the same job, fix the deployment too.
Branch 2: one execution ID, two items reach the action
If there is one execution ID but the email, API, or database node runs twice, inspect item counts node by node.
Common causes:
- a previous node returned two items;
- Split Out, Merge, or a loop expanded one logical record;
- two branches reconverged before the mutation;
- an expression referenced all items when the action expected one;
- a sub-workflow was called once per item.
Add a fail-closed assertion immediately before the expensive side effect. For a one-item contract:
const items = $input.all();
if (items.length !== 1) {
throw new Error(`Expected exactly one mutation item, received ${items.length}`);
}
return items;
This turns an invisible double charge or double post into an explicit failed execution. Route that failure through an error workflow. n8n's Error Trigger documentation explains how a linked workflow receives execution and error details for automatic failures.
The acceptance test is a payload designed to exercise every branch and loop while the mutation stays in dry-run mode. Require one proposed action receipt for one business key.
Branch 3: the same logical action was replayed
The third pattern often looks like two normal executions separated by a timeout, retry, restart, or manual rerun.
The first execution may have written successfully and then failed before recording completion. The retry does not know that the side effect already happened.
Do not deduplicate on $execution.id. A retry gets a different execution ID. Use a stable source key such as:
stripe:event_idshopify:order_idcrm:lead_id:stageschedule:workflow_id:expected_slot
Then claim ownership atomically before the side effect. In PostgreSQL, the critical property is the unique constraint plus a single atomic insert:
INSERT INTO automation_action_ledger (
idempotency_key,
status,
claimed_at
)
VALUES ($1, 'claimed', NOW())
ON CONFLICT (idempotency_key) DO NOTHING
RETURNING idempotency_key;
Only the execution that receives a returned row owns the mutation. A read-then-write check is not enough under concurrency because two workers can both read “missing” before either writes.
After the external action, update the ledger with a terminal receipt. If the action succeeds but the receipt write fails, recovery must reconcile against the external system instead of blindly replaying.
Also set an explicit production concurrency contract. n8n's self-hosted concurrency documentation distinguishes regular-mode production limits from queue-worker concurrency; neither replaces a business-level idempotency key.
The five-case fault test
Before calling the duplicate issue fixed, run at least these cases:
- The same source event arrives twice.
- Two workers claim the same key at the same time.
- The external action succeeds and the workflow fails immediately afterward.
- A node emits two items before the mutation.
- The workflow is manually retried with the original input.
For every case, store an expected receipt:
{
"idempotencyKey": "schedule:daily-report:2026-07-19T09:00Z",
"claimStatus": "duplicate_blocked",
"externalAction": "not_run",
"receiptStatus": "terminal"
}
The goal is not “green execution.” The goal is one authorized business outcome with evidence explaining every duplicate attempt.
A compact response checklist
- Two execution IDs at the same time: inspect active copies and main-process ownership.
- One execution ID with two items: inspect item fan-out and add a pre-mutation cardinality assertion.
- A later retry repeats an accepted action: add an atomic business-key ledger and recovery receipt.
- A green run produces the wrong or empty outcome: validate output shape, freshness, and completion—not only error status.
- A fix cannot survive the five fault cases: it is not finished.
Builderlog's free self-verifying n8n reference packet includes an inspectable replay ledger, atomic PostgreSQL claim, fault cases, recovery runbook, and verifier.
If you need the adaptation layer for your own workflow, the n8n Production Reliability Kit Pro.4 is currently $19 Solo for buyer #1 or $99 Team for up to 10 internal users. It adds the scope and topology workbooks, fourteen-case acceptance matrix, empty-branch receipts, ambiguous-retry reconciliation, incident/replay receipts, handoff checklist, and Team rollout controls.
No production credentials or customer data are required to use the templates.
Top comments (0)