Because Meta's webhook does not guarantee exactly-once delivery. The documentation says Meta retries when a delivery fails and that your server should handle the repeats. In my tests on Instagram, I saw the same event arrive twice, 2 to 71 milliseconds apart, too fast to be a retry after a failure, and the documentation does not explain that case. Finding the cause fixes nothing. What fixes it is making the flow recognize that it has already seen that message. I checked the documentation on October 9, 2026.
What Meta documents
A webhook is a notification Meta sends to your system every time something happens, such as a new message. What the documentation says about failures:
- On WhatsApp, if your endpoint answers with anything other than 200, or the delivery fails for another reason, Meta retries with decreasing frequency for up to 7 days. It warns that these retries can produce duplicate notifications.
- In the Graph API and Instagram webhook documentation, Meta retries right away and then a few more times over 36 hours. The sentence is direct: your server should handle deduplication in these cases.
- The Messenger documentation asks for a 200 OK within 5 seconds.
- Notifications can arrive in batches of up to 1,000, and Meta says batching is not guaranteed, so each webhook should be handled on its own.
In short, the promise is at least once, not exactly once.
What I saw in practice
In a production flow that answers Instagram direct messages, pairs of executions of the same event showed up, 2 to 71 milliseconds apart. They were two parallel executions of the same flow, and each was about to reply. I do not know the cause: the documentation talks about retries after a failure, and here both deliveries arrived together. So I treat it as platform behavior and protect the flow, without betting on the reason.
Why the reply goes out twice
Two executions start almost at the same time. Each asks "have I already answered this message?". Neither has finished saving its reply, so both hear "no" and both reply. It is a race, and the result is the customer receiving the same answer twice.
Two attempts did not work in my test:
- Storing the IDs already seen in the flow's own state (in n8n,
$getWorkflowStaticData). Parallel executions read the empty state before any of them writes. - Limiting the flow to one execution at a time. That did not serialize the executions in my test.
What worked
A sequence of three steps before any reply: the trigger, a check, and n8n's own Remove Duplicates node.
Remove Duplicates, in the operation that removes items processed in previous executions, compares the new item with what was stored before. In "Value Is New" mode, it discards an item whose value has already appeared. The key I use is the message ID (message.mid, for direct messages) or the comment ID (value.id). Only the output with new items stays connected to the rest of the flow, so the duplicate copy ends there. The n8n documentation says it keeps 10,000 items by default, and that the scope can be the node or the whole workflow. At high volume, increase the history.
I validated this on two pairs of real executions: in each pair, only one of the two went on to reply.
The check that comes before Remove Duplicates discards three things:
- The echo. When the agent replies, its own reply comes back as a new event. The Instagram documentation covers these echo events, marked with
is_echo. Without this filter, the agent ends up talking to itself. - Read and delivery events, which are not messages.
- Messages with no text and no attachment. Without this filter, the AI agent receives an empty request and fails.
What to do
- Answer 200 quickly and leave the heavy work for later. If the flow takes longer than Messenger's 5-second window, it risks triggering a retry.
- Deduplicate by message ID, before any reply or write.
- When the flow is not in n8n, or when you need a strong guarantee, use a unique key in the database: the second record with the same ID fails, and the flow stops there.
- Test with two simultaneous sends of the same event before going live. I did not see a guarantee in the n8n documentation that the node is atomic across simultaneous executions. What I have is the test.
AI-powered WhatsApp support: how I build and run it.