Idempotent Message Identity

Table of Contents

+#+filetags: :pattern:nats:publication: +#+created: 2026-10-14 +#+updated: 2026-10-14 +#+pattern_group: publication

This page defines the nats pattern Idempotent Message Identity in the NATS service patterns language. It is the concrete form, on this bus, of Idempotent Receiver.

1. Also known as

2. Context

At-least-once delivery means a message can arrive twice: a durable consumer redelivers an unacknowledged message, and a publisher retries an unacknowledged send. Both are correct behaviour that the receiver must survive.

3. Problem

How does a receiver do the work once when the same message may reach it two or more times?

4. Forces

  • The transport guarantees at-least-once, never exactly-once.
  • The work is often a write, which is not naturally repeatable.
  • The receiver may see the repeat minutes later, on another replica.
  • A server-side deduplication window is bounded in time; the receiver cannot rely on it alone.

5. Solution

Give the message a stable identity derived from the work, and make the write idempotent on it. Two mechanisms are available and they compose:

// 1. The identity the server can dedupe on: Nats-Msg-Id on a JS publish.
std::unordered_map<std::string, std::string> hdrs{
    {"Nats-Msg-Id", std::string(step_id)}};
nats.js_publish(subject, payload, hdrs);

// 2. The receiver's own guard, which outlives the server's window.
if (store.has_completed(step_id))
    return;                       // already done: discard the duplicate
store.record_completion(step_id, result);

Three rules:

  • The identity comes from the work, not the send. A fresh id per attempt defeats the pattern.
  • The receiver's own guard is authoritative. `Nats-Msg-Id` and the server's duplicate window help, but the window is finite and a redelivery may fall outside it, so the receiver must be able to recognise a repeat itself.
  • The guard is checked in the same transaction as the write. A check followed by a separate write is a race between two replicas.

6. Failure handling

Concern What the pattern does
Errors A duplicate is discarded, not reported as a failure.
Timeouts A redelivery after AckWait is exactly the repeat this pattern absorbs.
Retries Retrying is safe by construction; that is the point.
Idempotency This pattern is the idempotency rule.
Observability The discarded repeat is logged at debug with its identity, so a duplicate storm is visible without being an error.

7. Pitfalls

  • An identity generated at send time. Each retry looks like new work.
  • Relying on the server's duplicate window alone. A repeat outside the window is applied twice.
  • A check and a write in separate transactions. Two replicas both pass the check.
  • Treating a duplicate as an error. It inflates the error rate for correct transport behaviour and hides the real failures.

8. Resulting context

Benefits
At-least-once delivery becomes effectively-once processing without the transport pretending to guarantee it.
Liabilities
The receiver must store the identity of completed work, which is durable state it would not otherwise need.

9. In ORE Studio

Both mechanisms are in use, on the messages that have an identity of their own.

The publisher side. `ores::nats::headers::with_publish_identity()` adds the `Nats-Msg-Id` header, and does nothing when handed an empty identity, so a message with no identity of its own is published without one rather than under a shared value. It is set from the work's own identifier:

Message Identity
A workflow step command The step id
A step completion (the workflow helper, the compute batch bridge) The step id
A workflow start (the shell, IAM, DQ, inbox, ORE, reporting) The run's instance id

A market tick and a liveness notice carry none: two ticks are genuinely two messages, and deduplicating them would discard real data.

The receiver side. Each durable consumer already holds its own guard, which is the authoritative one because the stream's window is finite:

  • IAM's provisioning step reads the step's recorded outcome before doing the work and replays it rather than repeating it.
  • The engine's step-completed handler treats an event for a step that is no longer in progress as a duplicate and keeps the state it recorded.
  • The engine's start handler recognises a start for a run that already exists and leaves the run alone.

One limit is worth stating: a guard that reads and then writes is not atomic against two deliveries running at once on different replicas, so a redelivery that overlaps the original is still possible. Closing that needs a claim in the store rather than a header, and the step's own record is the natural place.

10. Related patterns

11. Sources

12. See also

  • NATS service patterns — the structure note that orders this pattern language, and where to read this page in it.

Emacs 29.3 (Org mode 9.6.15)