Skip to content

Idempotency

Idempotency means that processing the same message more than once does not create incorrect duplicate effects. For example, writing the same event twice should not create two business records when the event represents one real business action.

In operational or defence logistics systems, the same principle might mean that one movement report, status update, sensor-derived alert, or audit event should not create duplicate downstream facts simply because JetStream redelivered it. Redelivery is a reliability mechanism, not a signal that the business event happened twice.

JetStream redelivery is normal in an at-least-once system. Redelivery may happen after a process crash, NATS reconnect, ACK failure, destination timeout, or successful destination commit followed by process failure before ACK.

The package prefers safe duplication over silent loss.

Why Idempotency Is Required

nats-sinks ACKs after the destination commit. That avoids silent loss, but it also means there is a small window where the destination has committed and the ACK has not yet reached JetStream. If the process stops in that window, the message can be redelivered. A correctly designed sink treats that duplicate as safe.

sequenceDiagram
    participant JS as JetStream
    participant R as Runner
    participant D as Destination

    JS->>R: Deliver message #42
    R->>D: Commit message #42
    D-->>R: Commit succeeded
    R--xJS: Process exits before ACK
    JS->>R: Redeliver message #42
    R->>D: Duplicate-safe write

The duplicate must not corrupt state or create unwanted business effects.

Common Strategy Names

The framework uses shared strategy names so future sinks can document familiar behavior even when their destination-specific implementation differs.

stream_sequence

Recommended for JetStream sinks. It uses:

  • stream name,
  • stream sequence.

This pair is stable for a message in a stream and maps naturally to a primary key.

When mirrors, sources, subject transforms, or rebuilt streams are part of the event path, confirm which stream name and sequence number the sink actually observes. stream_sequence is intentionally scoped to the consumed stream. If the same business event may reach the sink through more than one topology path, prefer a stable producer Nats-Msg-Id or a strict payload_field key. Topology-specific guidance is documented in Advanced JetStream Topology.

message_id

Uses a stable NATS message ID header. This is useful when producers already enforce message IDs.

payload_field

Extracts a field from the normalized JSON payload value. This is useful for business keys such as order_id, but it requires every message to carry the expected field.

When a message body is non-JSON text or bytes, JSON-capable sinks may wrap it in the nats-sinks JSON payload envelope. In that case, payload_field paths refer to the envelope. For example, payload would refer to the wrapped text value. For encrypted text and opaque payloads, prefer stream_sequence or message_id idempotency because those strategies do not depend on decrypting or interpreting the payload body.

Headers-only JetStream consumers are another case where payload-derived idempotency is unsafe. When JetStream omits the message body and exposes only Nats-Msg-Size, many different source messages can arrive at the runner with the delivered body b"". NatsEnvelope.idempotency_key() therefore rejects the payload-hash fallback when payload_omitted=true and no stream sequence or message ID is available. Use stream_sequence or producer Nats-Msg-Id for headers-only workflows.

Duplicate Handling Modes

Duplicate handling is destination-specific. A relational database might use an upsert or an insert-that-ignores-conflicts. An object store might use deterministic object keys. The HTTP sink uses an idempotency-key header and requires the target service to honor it for safe duplicate redelivery.

The important rule is destination-neutral: a sink must either make duplicate redelivery safe or clearly document that a mode is not suitable for normal production use.

For mission-support systems, prefer modes where duplicates are detectable and boring. A visible duplicate that is safely ignored is usually far easier to explain during recovery or audit than a missing event that was ACKed too early.

Production Recommendation

Use stream_sequence unless you have a strong reason to use producer-supplied IDs. For each destination, choose a write mode that treats duplicate delivery as success rather than creating duplicate business effects.

Current Scope

The current production implementations are Oracle Database, Oracle MySQL, FileSink, SpoolSink, HTTP, and S3-compatible object storage. Oracle duplicate handling is documented in Oracle Sink, including merge, configurable merge update columns, insert_ignore, insert, and append behavior. Oracle MySQL duplicate handling is documented in Oracle MySQL Sink, including upsert, insert_ignore, idempotency key columns, and table routes. The experimental Oracle NoSQL Database sink documents K/V-style table duplicate behavior in Oracle NoSQL Database Sink, including conditional skip_existing, unconditional replace, and fail_existing policies. The experimental Oracle Coherence Community Edition sink documents K/V duplicate behavior in Oracle Coherence Community Edition Sink, including skip_existing, replace, and fail_existing policies. File duplicate handling is documented in File Sink, including deterministic file names and skip_existing, overwrite, and fail policies. Edge spool duplicate handling is documented in Edge Spool Sink, including deterministic idempotency-key filenames, encrypted replay records, skip_existing, and replay cleanup after target sink success. HTTP idempotency-key propagation, timeout ambiguity, and retry guidance are documented in HTTP Sink. S3-compatible object key construction, conditional create behavior, duplicate policies, and timeout ambiguity are documented in S3-Compatible Object Sink. Destination-specific key-column, filename, or payload-field details are kept on the sink pages so future sinks can document their own backend-native approach without changing this generic guide.

Oracle table routes may also override the sink-level idempotency policy. That lets one Oracle sink process several subject families while using stream sequence keys for one table, message IDs for another, and payload-field keys for a third. Route overrides are validated before processing starts, and the core still ACKs only after the full Oracle transaction commits.

The generic core also protects idempotency by owning all acknowledgement decisions. Sinks never receive raw NATS messages and cannot ACK early. If a sink raises before durable success, the core leaves the message eligible for redelivery. If a permanent failure is sent to a DLQ, the original message is ACKed only after DLQ publication succeeds.

Fan-out delivery adds another boundary to review. Required route targets remain part of the formal commit-then-ACK path and therefore need normal idempotency guarantees. Optional route targets are explicitly side copies unless they complete before the ACK gate releases. Optional targets should still be idempotent, but operators must not use an optional target as the only durable record for business or mission state.

Roadmap

Future idempotency work should focus on certifying every new sink, not only on adding more strategy names. Planned areas include:

  • additional certification evidence for complex multi-route Oracle idempotency deployments,
  • richer duplicate counters and metrics where backend drivers expose reliable committed outcomes,
  • expanded HTTP and S3 live certification guidance for reviewed endpoints,
  • a reusable sink certification test suite that proves duplicate redelivery is safe before a sink is called production-ready.