Description
What is at-least-once delivery?
At-least-once delivery is a guarantee about what a messaging system does when something fails: every message will be delivered to its consumer one or more times. Nothing is silently dropped, but duplicates are allowed. It sits between at-most-once delivery (send and forget: never duplicated, sometimes lost) and the much-advertised exactly-once delivery, which across independent systems is achievable only as an end-to-end effect, not as a property of the transport.
The guarantee comes from where the acknowledgement happens. A consumer that acknowledges only after it has finished processing will see the message again if it crashes, times out or loses its connection before the ack lands — the sender cannot tell "processed but ack lost" from "never processed", so it redelivers. Most Message Queue products and log-based streams work this way by default, and so does Meta's webhook delivery: under its Webhook Retry Policy, a notification that does not get a 200 is retried with decreasing frequency for up to seven days, and because retries interleave with new events, webhook event ordering is not guaranteed either.
The practical answer is an idempotent consumer: processing the same message twice has the same effect as processing it once. That is what an Idempotency Key and Webhook Deduplication implement — at-least-once delivery is the system-wide guarantee you design for, and those are the techniques that make it safe. Outbound sends have the same shape: a send to the Cloud API that times out may or may not have been accepted, so retrying it makes your outbound path at-least-once too.
Key takeaways
- At-least-once delivery means no message is silently lost, but any message may arrive more than once.
- Meta's WhatsApp webhooks are at-least-once: failed deliveries are retried for up to 7 days, and order is not guaranteed.
- The consumer acknowledges only after processing, so a crash between doing the work and sending the ack replays the message.
- Exactly-once is at-least-once delivery plus an idempotent consumer — not a mode a queue can give you on its own.
- Make side effects idempotent with a unique key, such as the message
idplus status, committed in the same transaction as the work.
Usage
At-least-once delivery in code
import pg from "pg";const pool = new pg.Pool();// msg = entry[].changes[].value.messages[i] from a WhatsApp webhookexport async function handleInbound(msg) {const client = await pool.connect();try {await client.query("BEGIN");const { rowCount } = await client.query("INSERT INTO processed_events (event_key) VALUES ($1) ON CONFLICT (event_key) DO NOTHING",["msg:" + msg.id],);if (rowCount === 0) { // seen before: a retry or a duplicateawait client.query("ROLLBACK");return "duplicate"; // still answer 200 so Meta stops retrying}await applySideEffects(client, msg); // credit, ticket, reply job...await client.query("COMMIT"); // key and effect land togetherreturn "processed";} catch (err) {await client.query("ROLLBACK");throw err; // let the queue redeliver} finally {client.release();}}// Statuses: sent, delivered and read share one message id, so key on bothexport const statusKey = (s) => "status:" + s.id + ":" + s.status;
The dedupe insert and the side effect commit in one transaction, so a crash can never leave one without the other.
Mechanics
How does at-least-once delivery work?
step 1 — The sender hands over a message
A queue publisher, or Meta's webhook sender, transmits the message and keeps responsibility for it until the consumer confirms. Until then the message is in flight.
step 2 — The consumer processes, then acknowledges
Your worker does the work — stores the status, calls the CRM, queues a reply — and only then acks: returns HTTP 200 to Meta, commits the offset, or deletes the queue message.
step 3 — A failure between work and ack causes redelivery
If the process crashes, the ack is lost or a visibility timeout expires first, the sender cannot know the work was done, so it delivers the message again — possibly to another worker, possibly out of order.
step 4 — Deduplicate on a stable key
Before running side effects, insert the message's identity — the inbound message
id, theidplusstatusfor a status webhook, or your own idempotency key — into a table with a unique constraint. A conflict means "already done": acknowledge and skip.step 5 — Commit the key and the effect together
Write the dedupe key in the same database transaction as the state change it guards, or use a Transactional Outbox. If they commit separately, a crash between them recreates the duplicate — or the loss — you were avoiding.
Properties
At-least-once delivery: key facts and limits
- Guarantee
- Delivered one or more times; never silently lost
- Duplicates come from
- Lost acks, crashes before the ack, timeouts, retries
- WhatsApp webhook retries
- Decreasing frequency, for up to 7 days
- WhatsApp webhook ordering
- Not guaranteed
- Dedupe key, inbound message
- messages[].id (the wamid)
- Dedupe key, status
- statuses[].id + statuses[].status
- Exactly-once in practice
- At-least-once + idempotent processing
Caveats
Common mistakes with at-least-once delivery
Trusting a broker's exactly-once label
Exactly-once features in brokers cover reads and writes inside that broker. The moment your consumer calls the Cloud API, a CRM or a payment gateway, a crash can still repeat the call. Make the external effect idempotent.
Deduplicating in memory
An in-process set of seen IDs forgets everything on restart and is not shared between pods — exactly when duplicates arrive. Use a database unique constraint, or a shared store whose expiry outlasts the retry window.
Acknowledging before the work is durable
Returning 200 to Meta and then processing in memory turns at-least-once into at-most-once: a crash loses the event. Persist it to a queue or table first, then acknowledge.
Keying statuses on the message ID alone
One message produces sent, delivered and read events with the same
id. Keying only on that ID drops the later statuses as duplicates; key onidplusstatus.Blindly retrying outbound sends
A send that timed out may already be on its way to the customer. Before resending a non-idempotent message such as an OTP or a payment link, check your send ledger and the status webhooks for that request.
Example
Example: Double cashback at a Surat wallet app
A payments app in Surat credits cashback when a customer replies YES to an offer on WhatsApp. During a deploy its webhook endpoint returned 502 for four minutes, and some replies had already been processed by a pod that died before it could answer 200. Meta retried those notifications, the new pods processed them again, and 37 customers were credited twice. The fix: the handler now inserts the inbound message id into a processed_events table with a unique constraint, inside the same transaction as the credit. Replays hit the constraint, are acknowledged with 200 and skipped — and a duplicate counter on the dashboard shows how often it happens.
Why it matters
Why does at-least-once delivery matter?
Treating webhooks or queue messages as exactly-once is how customers get two refund confirmations, two OTPs or a cashback credited twice. Treating them as at-most-once is how a delivered status or an inbound complaint disappears during a deploy. Naming the guarantee forces the right question for every handler — what happens if this runs twice? — and answering it once, with a dedupe key and a transaction, is far cheaper than reconciling duplicate side effects after an outage replays a day of events.
Questions
At-Least-Once Delivery FAQs
- Are WhatsApp webhooks delivered exactly once?
- No. Meta delivers webhooks at least once: a notification that does not get a 200 response is retried for up to seven days, and order is not guaranteed. Deduplicate inbound messages by
idand statuses byidplusstatus. - Is exactly-once delivery possible?
- Not as a pure transport guarantee across independent systems, but it is achievable as an effect. Combine at-least-once delivery with idempotent processing — a unique key committed in the same transaction as the side effect — and repeats become harmless.
- When is at-most-once delivery acceptable?
- When losing an occasional message costs less than handling duplicates. Typing indicators, live dashboard ticks and sampled analytics fit; payments, OTP audit trails, delivery statuses and customer messages do not.
- How is at-least-once delivery different from an idempotency key?
- At-least-once delivery is the guarantee your system gives; an idempotency key is one technique that makes it safe. The guarantee says duplicates will happen, and the key lets the consumer recognise and ignore them.
Notes
Best practices for at-least-once delivery
- Write down the delivery guarantee of every hop — webhook, queue, worker, outbound API. The weakest hop is your system's guarantee.
- Keep dedupe keys at least as long as the longest redelivery window; for WhatsApp webhooks that is seven days.
- Make status handling order-independent: never move a message backwards from read to delivered, whatever order events arrive in.
- Count duplicates as a metric; a sudden rise usually means slow acknowledgements or a crashing worker.
- Cap redeliveries: a message that fails every attempt belongs in a dead-letter queue with its last error, not in an endless retry loop.