01 · How it works, in 6 steps
How does webhook deduplication work?
- 1
Derive an event key
For each item, build its key:
msg:<wamid>for inbound messages,st:<wamid>:<status>for statuses, and a field-specific composite for everything else. Compute it from the event, never from the HTTP body as a whole. - 2
Claim the key atomically
Use one operation that succeeds for exactly one caller:
SET key 1 NX EX <ttl>in Redis, orINSERT … ON CONFLICT DO NOTHINGagainst a unique index. Never check-then-insert in two steps. - 3
Run side effects only on a fresh claim
If the claim succeeded, process the event. If the key already existed, acknowledge and skip — the work was done, or is being done, by someone else.
- 4
Keep keys at least seven days
Retries can arrive up to seven days later, so a 24-hour TTL lets late duplicates through. Keep keys for eight days to leave a margin.
- 5
Recover from half-finished work
Claim with a short-lived
processingstate and mark itdoneat the end. If a worker dies mid-way, the claim expires and the next retry can finish the job instead of being skipped forever. - 6
Measure the duplicate rate
Count skipped events per field. A rising rate points to slow acknowledgements upstream, long before customers notice double replies.
02Try it
Hands-on: use the controls to work through it yourself.
Send deliveries through a dedup check
12× slow motionService time S = 90 ms per job on average · 4 workers
Queue diagram: 1 deliveries waiting, 3 of 4 workers busy, 7 done, 1 duplicate, skipped.
- Backlog
- 0.8
- deliveries waiting on average
- Throughput
- 30/s
- 26.4/s done · 3.6/s duplicate, skipped
- Average wait
- 27 ms
- 39% of deliveries wait at all
- Utilisation ρ
- 0.67
- ≈ 2.7 of 4 busy
ρ = λ × S ÷ c = 30/s × 90 ms ÷ 4 = 0.67
Utilisation is below 1: the workers keep up.
03What is webhook deduplication?
Webhook deduplication is recognising a WhatsApp webhook event you have already handled and skipping it before anything happens twice. It is needed because Meta's model is at-least-once delivery, not exactly-once. Under Meta's webhook retry policy, a delivery that times out, gets a non-200 response or loses its connection is retried with decreasing frequency for up to seven days. Meta also notes that retries go to every app subscribed to the WhatsApp Business Account's relevant fields, so one app's failures can create duplicates for another. Even a perfectly healthy endpoint should expect to see the same event more than once.
Deduplication gives every event a stable identity and records it atomically before any side effect runs. The identity depends on the event: for an inbound message it is the WhatsApp message ID (wamid) in messages[].id; for a status it is statuses[].id plus status, because one wamid legitimately produces sent, delivered and read events; for other fields such as template or account updates it is a composite of the field, the entry[].id, the event's own timestamp and its key values. A POST can carry up to 1,000 updates and batching is not guaranteed, so the unit of deduplication is the event inside the payload, never the HTTP request.
It is easy to confuse with neighbouring ideas. An idempotency key protects your outbound requests — making sure a retried send does not message a customer twice — while webhook deduplication protects your inbound processing. And deduplication removes repeats of the same event; putting different events into the right sequence is the separate job of webhook event ordering.
Key takeaways
- Webhook deduplication discards repeated deliveries of the same WhatsApp event before any side effect runs.
- Meta delivers webhooks at least once and retries for up to seven days, so duplicates are normal rather than a bug.
- Key inbound messages on the wamid and statuses on wamid plus status, because sent, delivered and read share one wamid.
- Claim each key atomically, with SET NX or a unique index, and keep keys for at least seven days.
- Deduplicate each event inside a payload, never the HTTP request: one POST can batch up to 1,000 updates.
04 · In code
Webhook deduplication in code
const EIGHT_DAYS = 8 * 24 * 60 * 60;function eventKeys(value) {const keys = [];for (const m of value.messages ?? []) keys.push({ key: "msg:" + m.id, event: m });for (const s of value.statuses ?? []) keys.push({ key: "st:" + s.id + ":" + s.status, event: s });return keys;}export async function handleChange(value) {for (const { key, event } of eventKeys(value)) {// Atomic claim: only the first delivery gets "OK"const fresh = await redis.set(key, "processing", { NX: true, EX: 300 });if (!fresh) continue; // duplicate, or another worker has ittry {await applySideEffects(event);await redis.set(key, "done", { EX: EIGHT_DAYS });} catch (err) {await redis.del(key); // let a retry try againthrow err;}}}
Claim with a short TTL while working, then keep the key for eight days once done.
05 · Mistakes to avoid
Common mistakes with webhook deduplication
Keying statuses on the wamid alone
sent, delivered and read share one wamid. Deduplicating on the wamid keeps the first status and silently drops the rest. Include the status in the key.
Hashing the whole request body
A retry can batch events differently, so the same event arrives inside a different body. Deduplicate each event, not each POST.
Check-then-insert races
Two workers both read "not seen" and both process. Use an atomic set-if-absent or a unique constraint so only one claim can win.
Keys that expire too soon
Retries continue for up to seven days. A 24-hour TTL is the most common reason duplicates still appear after an outage.
06Example: Exam-results day at a Kochi ed-tech
A Kochi ed-tech company's webhook endpoint goes down for 40 minutes on exam-results morning, while students are replying to their results messages. When it recovers, Meta's retries arrive mixed with live traffic, and some events that had reached the endpoint just before the crash are delivered again. Its workers claim msg:<wamid> keys in Redis before calling the chatbot, so each student's question is answered once; the dashboard shows about one in ten events skipped as duplicates for the next hour, then the rate falls back to near zero (figures illustrative).
07 · Key facts
Webhook deduplication: key facts and limits
- Delivery guarantee
- At least once
- Retry window
- Up to 7 days
- Key lifetime
- 7 days minimum; 8 for margin
- Inbound message key
- messages[].id (the wamid)
- Status key
- statuses[].id + status
- Updates per request
- Up to 1,000; batching not guaranteed
08 · Why it matters
Why does webhook deduplication matter?
Every duplicate that slips through repeats a side effect. The chatbot answers the same question twice — and each answer is a charged message. A CRM opens two tickets, a loyalty programme awards points twice, an analytics pipeline double-counts conversions, a payment confirmation triggers two shipments. After an outage, Meta's retries arrive alongside live traffic and duplicates can briefly make up a large share of what you receive, which is exactly when a team least wants to clean up by hand.
09 · Pro tips
Best practices for webhook deduplication
- Deduplicate as close to the side effect as possible, so with webhook fan-out every consumer is protected, not only the first.
- Log skipped duplicates with their key — they are the cleanest evidence of a slow webhook acknowledgement.
- Pair deduplication with status ranking, so a late but genuine status is merged rather than discarded.