Commit Through the Stream
Table of Contents
+#+filetags: :pattern:nats:publication: +#+created: 2026-10-14 +#+updated: 2026-10-14 +#+pattern_group: publication
This page defines the nats pattern Commit Through the Stream in the NATS service patterns language. It specialises Queue-Based Load Leveling and Idempotent Receiver for a publisher that cannot afford to lose a message.
1. Also known as
- NATS: JetStream publish, publish acknowledgement.
2. Context
A service publishes a message that is a fact in its own right: a workflow step command, a market tick, a completion event another service acts on. The consumer may be restarting, and the publisher then does something it cannot undo — deletes a retry record, advances a state machine, forgets the work.
3. Problem
How does a publisher know that a message it must not lose has actually been stored, before it acts as though the work were handed over?
4. Forces
- Core NATS `publish()` is fire-and-forget: it returns as soon as the bytes are buffered, with no confirmation from the server and no subscriber required.
- A publisher that proceeds on an unconfirmed send destroys the only record that the work is outstanding.
- A stream holds a message for a consumer that is not running.
- A publish acknowledgement costs a round trip, which a message whose loss is harmless does not warrant.
5. Solution
Send with `js_publish()` when the message's loss would be a defect, and let the call's failure change what the publisher does next:
try { nats.js_publish( step_completed_event::nats_subject, ores::nats::default_wire_codec().encode(evt), forwarded_caller_headers(msg)); } catch (const std::exception& e) { // The completion was not stored. Do NOT clear the retry record: // leave it so the poll that owns this work tries again. BOOST_LOG_SEV(lg(), error) << "step completion not stored: " << e.what(); return; } clear_batch_link(step_id);
Three rules define the pattern:
- The durable message is the one whose loss is a defect. A workflow completion, a step command, a tick a corpus must not lose. A liveness heartbeat, a cache-invalidation hint or a "something changed" nudge is not, and stays on `publish()`.
- The subject must be covered by a stream. A JS publish to an uncovered subject fails, so the stream is a precondition; see Stream Provisioning at Start.
- The failure changes behaviour. The throw is not decoration. Whatever the publisher does after the send — clearing state, reporting success, moving on — must not happen when the send failed.
6. Failure handling
| Concern | What the pattern does |
|---|---|
| Errors | `js_publish()` throws when the server does not acknowledge storage. |
| Timeouts | The publish is synchronous; a server that does not acknowledge blocks the caller until the client's timeout. |
| Retries | The caller retries, or leaves its own retry record in place so a later poll does. |
| Idempotency | A retry may store the message twice; give it an identity so the consumer can discard the duplicate. |
| Observability | The publisher logs the prefixed subject, and the failure when the send throws. |
7. Pitfalls
- A core `publish()` for a durable message. The call succeeds with no consumer, no stream and no server confirmation, and the publisher then destroys its own retry path. This is the failure the pattern exists to prevent.
- Catching the throw and proceeding. The error is logged, the state is cleared, and the work is lost with a clean-looking log.
- `js_publish` for everything. Every telemetry and liveness message now costs a round trip and a stream entry, and the streams grow without purpose.
- A JS publish to an uncovered subject. It throws, which is correct, but the reason is a provisioning gap rather than a transport fault.
8. Resulting context
- Benefits
- "Sent" means stored, so the publisher can safely forget the work.
- Liabilities
- A synchronous round trip per message, and a dependency on a stream existing.
9. In ORE Studio
The workflow step command is committed with `js_publish` by the engine, and `workflow_helpers.hpp` commits the step completion the same way. The `start_workflow` message is committed by every service that begins a run.
10. Related patterns
- Idempotent Message Identity — what makes the retry safe.
- Stream Provisioning at Start — the stream the publish needs.
- Acknowledge What Was Handled — the consumer half of the guarantee.
11. Sources
- NATS documentation, Publishing — the publish acknowledgement.
- NATS documentation, Delivery and acknowledgment — at-least-once from the reader's side.
- Gregor Hohpe and Bobby Woolf, Enterprise Integration Patterns, Addison-Wesley, 2003, Guaranteed Messaging.
12. See also
- NATS service patterns — the structure note that orders this pattern language, and where to read this page in it.