Commit Then Acknowledge
JetStream consumers must follow a commit-then-acknowledge processing model whenever they persist data, modify downstream state, or trigger durable business actions.
The acknowledgement to JetStream is the final step in message processing. It must be sent only after all required work has completed and the resulting state has been durably committed. An ACK is a formal statement that the message has been fully handled and no longer requires redelivery.
In mission-oriented systems, that ACK can be read as an operational statement: the event has crossed the agreed durable boundary. Sending it early can hide a failed database write, missing file, or incomplete handoff from later recovery and audit work.
Required Order
- Receive the message.
- Validate that the message can be processed.
- Execute the required business logic.
- Persist or commit all required durable state.
- Acknowledge the message only after successful completion.
sequenceDiagram
participant JS as JetStream
participant R as Runner
participant S as Sink
participant D as Destination
JS->>R: Deliver message
R->>R: Validate and normalize
R->>S: write_batch(...)
S->>D: Write destination state
D-->>S: Commit succeeds
S-->>R: Success
R->>JS: ACK
Why Early ACK Is Unsafe
Acknowledging too early creates a silent-loss risk. JetStream may consider the message handled even if the destination write fails afterward. A duplicate caused by redelivery is usually manageable with idempotency. A missing write after early ACK is much harder to detect and repair.
For defence logistics, operational reporting, or other sensitive workflows, silent loss is usually worse than safe duplication. A duplicate can be detected, reconciled, and explained. An event that was ACKed before durable success may simply disappear from the processing path.
Failure Before Commit
sequenceDiagram
participant JS as JetStream
participant R as Runner
participant S as Sink
participant D as Destination
JS->>R: Deliver message
R->>S: write_batch(...)
S->>D: Write attempt
D-->>S: Error before commit
S-->>R: TemporarySinkError
R-->>JS: No ACK
JS-->>R: Redelivery later
Failure After Commit But Before ACK
If the destination commit succeeds and the process exits before ACK, JetStream may redeliver. This is acceptable. The sink must use idempotency controls to treat the duplicate safely.
Multi-Sink ACK Gates
Fan-out delivery can select more than one logical sink target for the same message. The same safety rule still applies: JetStream may be ACKed only after every selected required target has durably completed. Route targets are required by default.
Optional targets are different. They are opt-in side effects with a bounded
minimum_wait_ms and timeout_ms policy. The ACK gate gives those optional
targets a controlled chance to finish, but it does not wait forever. If an
optional target has not committed before the ACK gate releases, the required
delivery path may still ACK and the optional copy must be treated as not
guaranteed.
This distinction is important for audit language. A required Oracle Database sink can be part of the formal custody path. An optional diagnostic file copy, secondary archive, or non-critical side copy is best-effort unless it completed before ACK. Operators should use optional targets only when that trade-off is acceptable and documented.
Permanent Failure With DLQ
sequenceDiagram
participant JS as JetStream
participant R as Runner
participant S as Sink
participant Q as DLQ
JS->>R: Deliver invalid message
R->>S: write_batch(...)
S-->>R: PermanentSinkError
R->>Q: Publish DLQ record
Q-->>R: Publish succeeds
R->>JS: ACK original
If DLQ publish fails, the original message is not ACKed.
Terminal Acknowledgements
NATS also supports terminal acknowledgements (AckTerm) and next-message
acknowledgements (AckNext). They have different safety properties and must
not be treated as ordinary sink-success ACKs.
AckTerm stops redelivery without marking the message as successfully
processed. nats-sinks supports it only as an explicit opt-in DLQ policy:
when dead_letter.ack_term_after_publish is true, the runner publishes the DLQ
record first and sends AckTerm only after that publication succeeds. The
default remains DLQ publication followed by normal ACK. AckTerm must never be
sent before sink success, before DLQ publication success, or for temporary
failures.
AckNext acknowledges a pull-consumer message and requests more messages in
one protocol operation. That is not a good fit for nats-sinks production sink
processing because the runner already controls fetch size, batch timeout, and
backpressure explicitly. Keeping fetch separate from ACK makes the safety
boundary easier to review and test.
The full decision is documented in ADR 0005: AckTerm And AckNext Evaluation.
Confirmed Acknowledgements
Some NATS clients support a confirmed ACK operation, often called AckSync or
double ACK. Confirmed ACK waits for the server to confirm that it processed the
ACK. That can improve operational visibility, but it does not move the safety
boundary.
For nats-sinks, confirmed ACK is still the final step. It is available as a
disabled-by-default delivery.ack_confirmation option after durable sink
success or successful DLQ publication, but it never runs before
sink.write_batch(...) returns success and it must never be used to claim
exactly-once delivery. If the sink commit succeeds and ACK confirmation then
times out, the message may redeliver and idempotency must handle the duplicate.
The evaluation, configuration, metrics, and AckTerm limitation are documented
in Acknowledgement Confirmation.
InProgress Signals
JetStream InProgress signals tell the server that a delivered message is
still being worked on and that the acknowledgement wait window should be
extended. They are not success acknowledgements. They must never replace final
ACK, NAK, Term, retry, or DLQ behavior.
For nats-sinks, delivery.in_progress is optional, disabled by default,
bounded, and owned by the core runner. When enabled with safe AckWait
configuration, the runner sends progress heartbeats only while
sink.write_batch(...) is active and stops before final ACK, NAK, Term, retry,
or DLQ handling. The sink still only returns durable success or raises an
error. If the sink fails after one or more progress signals, the message
remains eligible for redelivery or DLQ according to policy.
The evaluation and recommended implementation split are documented in InProgress Evaluation.
Push Consumers
JetStream push consumers deliver messages to a delivery subject instead of waiting for the client to fetch a bounded batch. That does not change the ACK rule. The opt-in push runner mode uses manual acknowledgement only and ACKs only after the sink reports durable success or after DLQ publication succeeds for permanent failures.
Push mode is disabled by default and guarded by bounded callback intake plus pending message and byte limits. Focused certification tests prove ACK-after-commit ordering, no ACK on temporary failure, DLQ-before-ACK on permanent failure, callback error containment, flow-control and heartbeat configuration propagation, and shutdown behavior. The evaluation and implementation split are documented in Push Consumer Evaluation.
Non-Negotiable Invariant
A JetStream message must only be acknowledged after all required durable side effects have completed successfully. ACK is the final confirmation of successful processing, never a prerequisite for processing.
Short slogan:
Commit first. ACK last. Design for redelivery.