I spent a while chasing a bug where a scheduled n8n workflow was dropping items — not crashing, not erroring in a way I noticed, just… quietly not processing some of them. The execution list looked healthy. Re-running did nothing. Here's what was actually happening, because I think a lot of people have this bug and don't know it.
The setup
A really common pattern: you keep a list of "already processed" ids in workflow static data so you don't re-send things. So the flow is:
mark id as done → do the work (send message / call API / write row) → done
Intuitive. Also wrong, and here's the specific reason.
The cause
n8n persists static data on the workflowExecuteAfter hook, and that hook fires whether the run succeeded or failed. I went and read it in the source to be sure, then reproduced it: a workflow that marks an id done and then throws still has the "done" write persisted.
So when the work step fails:
- the id is marked done ✅
- the work never happened ❌
- the run shows as failed, so you think you'll just retry it
- but on retry, the id is already "done" → it's skipped forever
The item is gone. Nothing tells you. The counter of "processed" even looks right.
The fix is ordering, not code
Commit after the side effect succeeds, per item:
check if done → do the work ✅ → only now mark it done
Concretely, in a Code node it's just moving the write to the end:
// runs only AFTER the side effect succeeded, once per item
const state = $getWorkflowStaticData('global');
state.done = state.done || [];
const key = String($json.id);
if (!state.done.includes(key)) state.done.push(key);
return $input.item;
Two details that matter in practice:
- Per item, not per batch. If you process 10 items and mark the whole batch done at the end, one failure in the batch re-does the other 9 on the next run (and re-sends 9 messages). Commit each item right after its own side effect.
- It's at-least-once, not exactly-once. If the process dies between the side effect and the commit, that one item repeats. That's the right trade for most work — a visible duplicate beats a silent loss — but know that you're choosing it.
Why this one is worth your attention
The thing I keep coming back to: this isn't an exotic edge case. It's the default way most people wire dedup/idempotency in n8n, and it fails in exactly the situation you built the dedup for (something downstream breaks). The green checkmark is the trap.
If you have a scheduled workflow with a dedup list right now, go look at the order of those two steps. It takes thirty seconds and it's either fine or it's been quietly eating your data.
I write these up as I hit them while building production n8n workflows. This one, plus five other error-handling patterns (smart retry, cooldowns, dead-letter, circuit breaker, graceful degradation) — each with the experiment and the number behind it — ended up in a kit I put together: Error-Handling Starter Kit for n8n. There's also an invoice-processing template built on the same idea: validate the output, don't trust it.
Top comments (0)