Socioverse

Loading…

Why Your Comment-to-DM Automation Sends the Same Message Twice

By Anshul Rastogi on 2026-09-22

The bug that looks like a spam problem

Someone comments the keyword on your reel. They get your DM. Then they get it again. Sometimes a third time, a few minutes apart, identical text, same link.

The creator's first instinct is that the automation is misconfigured, so they go and look at the trigger, the keyword, the cooldown field. Everything is set correctly. The automation is doing exactly one thing per event. The problem is that Instagram sent the event more than once, and nothing in the tool noticed.

This matters more than an embarrassing double message. Sending the same block of text to the same person repeatedly is the precise behaviour Meta's Community Standards on spam describe as acting "either manually or automatically, at very high frequencies." A duplicate-send bug in the trigger layer does not stay a cosmetic bug. It becomes an account-safety problem, and the creator never connects the two, because the tool's dashboard shows one automation, one send.

Every Meta-attributed quote below is taken from Meta's own developer documentation and Community Standards, linked in Sources, accessed 13 September 2026.

First principle: a webhook is a delivery attempt, not an event

The mental model almost everyone carries is that a webhook is a notification. Something happened, Instagram told you, you act on it. One happening, one telling.

That is not what a webhook is. A webhook is an HTTP request that Meta will keep making until your server convincingly says it arrived. Meta's own webhooks documentation is explicit about the retry behaviour:

"If any update sent to your server fails, we will retry immediately, then try a few more times with decreasing frequency over the next 36 hours."

And about what counts as the acknowledgement:

"Your endpoint should respond to all Event Notifications with 200 OK HTTPS."

"Unacknowledged responses will be dropped after 36 hours."

Read those together and the consequence is unavoidable. Anything other than a prompt 2xx means the same comment arrives again. Not a similar comment. The same comment, with the same id, hours later, looking to your handler exactly like a fresh one.

This is the delivery model distributed systems call at-least-once. The platform guarantees you will not miss the event. It does not guarantee you will see it only once. Meta does not leave this to inference. In the same documentation, on batching, it states the obligation directly:

"Event Notifications are aggregated and sent in a batch with a maximum of 1000 updates. However batching cannot be guaranteed so be sure to adjust your servers to handle each Webhook individually."

"Your server should handle deduplication in these cases."

That last sentence is the whole article. Deduplication is documented as your job. "One comment, one DM" is not a property Instagram hands you. It is software somebody has to write, and if nobody wrote it, the creator finds out through their audience.

What actually triggers the retry

The uncomfortable part is how ordinary the failure conditions are. A comment-to-DM handler does real work before it can answer: look up which automation matches, fetch the commenter's profile, check a follow relationship, decide on the reply, queue the send. Several of those are network calls to Meta.

So the handler is slow by nature, and every one of these produces a non-2xx or a timeout, and therefore a redelivery:

What happens What Instagram sees Result
A Graph call inside your handler is slow No prompt response Redelivered
Your function hits its execution timeout No response Redelivered
A database write fails on an unrelated field 500 Redelivered
A deploy restarts the process mid-request Connection dropped Redelivered
Your handler throws after the DM was already sent 500 Redelivered, and the DM sends again

That last row is the one that bites. The send succeeded. The handler crashed afterwards, on logging or analytics or a lead record. Meta never heard a 200, so it redelivers, and the tool, having no memory of the first attempt, sends the DM a second time. The creator's audience sees the bug. The dashboard does not.

Without an event claim a handler error after the DM makes Meta retry and the DM sends again; with a signature check and a unique event claim the retry hits 23505 and returns 200 without resending

Why a cooldown is not deduplication

Most comment-to-DM tools do ship something that sounds like protection. It is usually a per-user cooldown: do not message the same person again within N minutes.

A cooldown and an idempotency check are different mechanisms solving different problems, and the difference is not academic:

Cooldown Idempotency
Question it answers Have we messaged this person recently? Have we already handled this exact event?
Keyed on The recipient The event id
Controlled by The creator, in a settings field The system, never optional
At its default Frequently zero Always on

The failure is in that last row. A cooldown is a user-facing setting, and the value people choose is usually zero, because a cooldown that blocks real replies feels like the tool is broken. A cooldown of zero minutes expires the instant it is written. It stops nothing. The guard is present in the UI, visible in the settings, and doing no work at all against a redelivery.

Worse, a cooldown is keyed on the person. It cannot tell a redelivery of Tuesday's comment from that person genuinely commenting again on Wednesday. Whichever way it is tuned it is wrong: tight enough to stop duplicates means suppressing real second engagements, loose enough to allow real ones means passing duplicates through.

Deduplication has to be keyed on the event, not the recipient. That is the only key that separates "Instagram told us twice" from "they commented twice."

The four guards that make delivery exactly-once

This is how the Socioverse webhook receiver is built. It is not clever architecture. It is four specific, boring things that have to be in the right order.

1. Verify the signature before trusting anything in the body

Your webhook URL is a public HTTPS endpoint. Anyone who finds it can POST to it. Meta signs every payload so you can tell its requests from everyone else's:

"We sign all Event Notification payloads with a SHA256 signature and include the signature in the request's X-Hub-Signature-256 header, preceded with sha256=."

Socioverse computes an HMAC-SHA256 of the raw request body using the app secret and compares it to that header before a single field is read. A mismatch is a 403 and nothing else happens.

One practical detail that costs real debugging time: compare the hex case-insensitively. Meta does not always send the digest in the same case, and a strict string equality check rejects genuine Meta traffic intermittently, which presents as automations that mysteriously stop firing rather than as a signature error.

2. Claim every event exactly once, in the database

Every inbound event gets claimed against a stable id from the payload before any work is done:

The claim is an insert into a table with a unique constraint on the key, namespaced by event type. If the insert succeeds, this is the first time we have seen it, and processing continues. If the database returns 23505, a unique-constraint violation, this is a replay and the handler stops immediately and answers Instagram with a success.

The important design decision is that the claim is a database constraint, not an in-memory cache. Serverless functions do not share memory, and the instance that handled the first delivery is usually gone by the time the retry arrives thirty minutes later. An in-process set or an LRU cache looks like deduplication in testing and provides none in production. The uniqueness has to live where every instance can see it.

3. Fail open, not closed

If the idempotency check itself errors, for example the database is briefly unreachable, Socioverse processes the event anyway.

This is a deliberate trade, and it is worth stating plainly because it is the opposite of what a strict reading would suggest. The two failure modes are not symmetric. A rare duplicate DM is an annoyance. A dropped event is a lead who commented, got nothing, and never comes back, with no error anywhere for the creator to see. Dropping a real event is worse than a rare duplicate, so the guard degrades toward sending.

4. Ignore your own echoes

Instagram can deliver your own outbound messages back to you. Meta's Instagram webhook fields include message_echoes as a subscription, and the documentation describes the resulting notification as carrying is_echo and is_self set to true.

Without an explicit check, the DM your automation just sent arrives as an inbound messaging event. A handler that treats every inbound message as audience activity will process its own send as if the person had written to you, which can start a sequence, create a lead, or trigger a reply to yourself. Socioverse checks is_echo and discards those events before any branch runs.

The events that were invisible

One more finding from operating this receiver, because it is the failure nobody looks for.

A DM carrying only a photo, only a voice note, or only a shared reel has no message.text field at all. Handlers are typically written to read the text, match it against keywords, and branch. With no text, the branch never runs, and the person is not "handled incorrectly", they are invisible: no lead, no auto-reply, no reply signal handed to a running sequence.

For a creator whose audience replies to stories with voice notes or shares reels into the DMs, that is a meaningful slice of inbound engagement silently going nowhere. Socioverse now gives that branch a typed placeholder so the lead is created and the sequence advances, while keeping the distinction so an attachment is never rendered as if the person had typed those words.

What to check in your own setup

Whatever tool you use, these are answerable without access to its source:

  1. Comment the keyword on your own reel, twice, a minute apart. You should get two DMs. If you get one, the tool is deduplicating on the person rather than the event, and it is suppressing real engagement.
  2. Ask what happens when the tool's own send fails halfway. If the answer is "we retry", ask what stops Instagram's retry from stacking on top of it.
  3. Look at whether the cooldown field defaults to zero. If the only duplicate protection is a number the creator sets, there is no duplicate protection.
  4. Check your DMs for a message you did not send twice. The audience notices this before any dashboard does.

Frequently asked questions

Why did my Instagram automation send the same DM twice?

Almost always because Instagram delivered the triggering event more than once and the tool had no event-level deduplication. Meta retries any webhook that does not receive a prompt 200 OK and keeps trying for up to 36 hours, and its documentation states that your server should handle deduplication.

Is duplicate webhook delivery a bug on Meta's side?

No. It is the documented design. Webhooks are at-least-once delivery: the platform guarantees you will not miss an event, not that you will receive it only once. Handling the repeat is the receiving application's responsibility.

Can duplicate DMs get my Instagram account restricted?

They contribute to the risk. Meta's Community Standards on spam cover acting "either manually or automatically, at very high frequencies", and repeatedly sending identical text to the same people is a textbook inauthenticity signal. This is why a trigger-layer bug is an account-safety issue and not a cosmetic one.

What is the difference between a cooldown and deduplication?

A cooldown is keyed on the recipient and asks whether you messaged this person recently. Deduplication is keyed on the event id and asks whether you already handled this exact event. Only the second one can tell a redelivery apart from the same person genuinely engaging again.

What is is_echo in an Instagram webhook?

It marks a notification as a playback of a message your own application sent. Automations that do not check it can process their own outbound DMs as inbound audience messages.

Why would an automation ignore someone who sent a voice note?

Because a message containing only an attachment has no message.text, and keyword-matching logic reads the text field. With nothing to match, the branch silently does not run.

Sources and references

All Meta-attributed quotes were taken from the following pages, accessed 13 September 2026.

More from the blog

Related