Workflow reliability reviews

The dedupe that passed every test and was never a dedupe

16 September 2026 · a worked example, on one of my own workflows

Disclosure. I sell reliability reviews of n8n workflows, and this article exists partly to show how I work. The defect below is in my own workflow, not a client’s and not one built to be broken. The limits of what it establishes are set out at the end and are not decoration.

An n8n workflow deduplicated inbound leads using $getWorkflowStaticData(), n8n’s own documented way to keep a value between executions without standing up a database. It was tested for a full session and behaved correctly every time. Duplicates were caught. State survived across two completely separate Docker containers sharing only a named volume — the second run correctly detecting the first’s record. Retention pruning worked. The tests were repeated after a schema change and passed again.

Every one of those tests was sequential.

That is the whole problem, and it is not a testing failure so much as a category error. Sequential tests establish observed behaviour. The property the workflow actually depended on — that two executions cannot both pass the duplicate check with the same key — is a concurrency guarantee, and no number of sequential passes speaks to it.

Worth being precise about how this went wrong, because the tempting version is false. It is not that nobody noticed. The limitation was written down, in advance, in the project’s own documentation: concurrent-execution safety of $getWorkflowStaticData “has not been established — see the idempotency design doc’s open risk.” The risk was named in writing, and the documentation was upgraded from “design-level only” to “proven durable” anyway. Naming a risk is not the same as pricing it.

What the source says

Ten minutes in n8n 2.33.7’s own source settled it:

That is a read-modify-write race with last-write-wins. The decisive detail is the third one: no code inside a Code node can close this race, because the node never performs the write. Retry logic does not help. A better key does not help. Checking a second time before writing does not help. The gap is between the node finishing and n8n persisting, and that gap is not the workflow’s to control.

This is also why the database underneath does not rescue it. The reproduction ran on SQLite, and a reader on Postgres will reasonably ask whether isolation level changes the answer. It does not: the write is a whole-blob overwrite issued after the execution has ended, so there is no transaction still open for an isolation level to protect.

Predicted, then reproduced

Predicted from the source first, then reproduced on the first attempt:

Be precise about the second result, because it is both the more dangerous one and the one easiest to overstate. No lead was destroyed. What was destroyed was the memory that those leads had been seen — and nothing reports that. No error is raised, no execution is marked failed, and n8n’s execution list shows three clean green runs. The failure is silent, and it surfaces later as duplicates nobody can explain.

The same pattern, read off an export

This is detectable statically, without running anything. Against that workflow the analysis reports, among other findings:

[HIGH] STATIC_DATA_CHECK_AND_SET — State is kept in n8n workflow
       static data, which is not atomic
    nodes:      Idempotency Check (design-level)
    proof:      These Code nodes call $getWorkflowStaticData() and also
                write. n8n gives each execution an in-memory COPY of
                static data and writes the whole blob back when the
                execution ends — so a check-and-set built on it is a
                read-modify-write race. No recognised trigger here can
                fire concurrently, which narrows but does not remove the
                exposure — a manual re-run or a queue-mode instance
                still can.
    NOT proven: Whether the race has actually cost this client a record.
                That needs their execution history, which is not in the
                export. Also not proven: what the stored keys are — the
                node was not run.

Note the clause that makes the finding less alarming: “No recognised trigger here can fire concurrently.” The analysis volunteers it. A finding that only ever argues in one direction is a sales document with line numbers.

What this does not establish


If you want this done to yours

Send the export and tell me what the workflow is for. You will get the findings, what each one does not prove, and the NOT CHECKED list. If there is nothing worth reporting, I will say so.

alej.kump@gmail.com   or read what the review covers →