Skip to content
matthewcm.dev

    ↑ ↓ move · ↵ open · tab scopeopen graph →

    Message Delivery Semantics

    At-most-once, at-least-once and "exactly-once". Why the network forces you to choose between losing messages and duplicating them, and how idempotency makes duplicates harmless.

    Evergreentended planted 2 min read
    On this page

    You send a message to another service and don’t get an acknowledgement. Did the message get lost, or did the acknowledgement? You can’t tell. Every messaging system’s delivery guarantee is a decision about what to do next.

    The three guarantees

    GuaranteeStrategyRiskGood for
    At-most-onceSend once, never retryMessages can be lostMetrics, telemetry, live prices where the next update supersedes this one
    At-least-onceRetry until acknowledgedMessages can be duplicatedAlmost everything else: orders, emails, jobs
    Exactly-onceAt-least-once delivery plus deduplicationComplexityPayments, ledgers, anything where duplicates cost money

    “Exactly-once” is a careful lie

    Over an unreliable network (Fallacies of Distributed Computing), true exactly-once delivery is impossible. What systems actually offer is exactly-once processing: deliver at least once, and make sure processing a message twice has the same effect as processing it once. That property is idempotency.

    Making operations idempotent

    • Idempotency keys. The client generates a unique key per logical operation (Idempotency-Key: 7f3a…). The server records keys it has processed and returns the original result for a repeat. Payment APIs work this way.
    • Natural idempotency. “Set status to shipped” is idempotent; “add one to the counter” isn’t. Prefer setting state over applying deltas where you can.
    • Deduplication windows. Consumers remember recent message IDs and drop repeats.
    • Transactional outbox. Write the state change and the outgoing message in one database transaction, then publish from the outbox. This avoids “saved but never announced” and its opposite.

    A small example

    A user double-clicks “Place order” on a flaky connection. Without idempotency, two orders. With a key generated when the form loads, the second request returns the first order’s confirmation. The same thinking applies to the retry logic in a WebSocket client: replay from a sequence number, and make applying an already-seen update a no-op.

    In practice

    The question I now ask of any endpoint that changes state is: what happens if this is called twice? If the answer is “something bad”, it needs an idempotency key or a redesign. It’s one of the cheapest reliability wins there is. The wider picture of what can go wrong is in Types of Failure, and the underlying channel ideas in Message Passing.

    • #distributed-systems
    • #messaging
    • #idempotency
    • #queues
    • #reliability

    ↪ Links to (4)