What is at-least-once delivery? Webhook delivery guarantees

At-least-once delivery is a guarantee that every message is delivered one or more times, never zero times. The sender keeps retrying until it receives an acknowledgement. Nothing is lost, but if the acknowledgement is lost or late, the receiver gets the same message again. Webhooks, most message queues (SQS, RabbitMQ, Kafka with default settings) and job systems work this way.

sender                         receiver
  │── event evt_123 ──────────▶│  processes it, starts responding
  │        (timeout, no 200)   │
  │── event evt_123 (retry) ──▶│  gets it a second time
  │◀──────────────── 200 OK ───│

The three delivery guarantees

GuaranteeLost messages?Duplicates?How it's achieved
At-most-oncePossibleNeverSend once, never retry
At-least-onceNever (within the retry window)PossibleRetry until acknowledged
Exactly-onceNeverNeverAt-least-once delivery plus deduplication

Over a network, the sender can't distinguish "the receiver never got it" from "the receiver got it but the reply was lost". It has to choose: retry and risk a duplicate (at-least-once), or give up and risk a loss (at-most-once). True exactly-once delivery isn't possible on its own. What systems offer as "exactly-once" is exactly-once processing: at-least-once delivery combined with an idempotent receiver.

Why webhooks are at-least-once

A webhook provider sends an HTTP POST and waits for a 2xx. If it gets an error, a timeout or no connection, it retries. Duplicates appear whenever your endpoint did the work but the provider didn't see the 2xx in time:

  • The handler was slow and the provider's timeout fired first, often visible as a 504 Gateway Timeout at your proxy.
  • The handler threw after committing.
  • A deploy killed the process between commit and response.
  • The provider itself retried an internal failure.

How to consume at-least-once messages

  1. Deduplicate on the message ID. Store the event ID (id, X-GitHub-Delivery, webhook-id) under a unique constraint in the same transaction as the side effect. See idempotency.
  2. Acknowledge after storing, not after processing. Write the event to a queue or table, return 200, and process asynchronously. A short request means fewer timeouts and fewer duplicates.
  3. Don't assume order. Retries reorder events. Use timestamps or version numbers, or fetch current state from the provider.
  4. Handle the "at least" failing. Senders give up eventually. Failed messages should land somewhere you can see and replay them, such as a dead-letter queue.

"At least once" has a time limit

The guarantee only holds while the sender keeps retrying. Stripe retries for up to about three days, GitHub doesn't retry automatically, and many smaller providers try only a few times. If your endpoint is down longer than that, the guarantee quietly becomes at-most-once.

A webhook gateway that stores events before delivering them closes that gap. Webhook Relay's durable retries persist each webhook first, then retry with exponential backoff for up to 30 days on the Long schedule, to public endpoints or to servers on localhost and private networks. Retried and replayed deliveries are the same request, so your existing deduplication keeps working. On the sending side, the Webhook Relay Outbound Gateway keeps the original webhook-id on every retry, recovery and replay so your customers can deduplicate too.

Frequently asked questions

What is at-least-once delivery?

At-least-once delivery is a guarantee that every message will be delivered one or more times. The sender keeps retrying until it receives an acknowledgement, so a message is not lost, but it can arrive more than once if an acknowledgement is lost or late.

What is the difference between at-least-once and exactly-once delivery?

At-least-once can deliver duplicates. Exactly-once means each message has its effect exactly one time. Over an unreliable network, exactly-once delivery is not achievable on its own; systems get exactly-once processing by combining at-least-once delivery with an idempotent receiver that ignores duplicates.

Are webhooks at-least-once?

Most webhook providers aim for at-least-once delivery: they retry when your endpoint fails or times out, which can produce duplicates. But their retry windows are limited, so if your endpoint is down longer than the window, events can still be lost.

How do I handle at-least-once delivery?

Make the consumer idempotent. Record each message ID with a unique constraint and skip IDs you have already processed. Acknowledge only after the message is safely stored, and don't rely on messages arriving in order.