Skip to content

What this module does not do

Each of these is a decision with a name, not a gap waiting to be filled. Several are easy to add, which is exactly why they are written down.

No persistence, no replay, no durability, by default

The bus itself keeps nothing: no queue of its own, no replay, no state to operate. That is a refusal rather than an omission. A bus that gains state has gained a component somebody has to operate: backup, migration, corruption, capacity, a second thing to be down.

What a backend can do beyond that, it declares. SurvivesRestart says whether an unacknowledged message outlives the process; Replays says whether a new durable identity can begin at the oldest message the backend still retains. The in-memory backend declares neither: nothing survives its restart, and a subscriber that starts late has missed what came before. A NATS backend on JetStream declares both, and a subscription opts in by naming a DurableName and a delivery Mode; a queue built on SQS, RabbitMQ or AMQP declares SurvivesRestart without Replays, because a queue delivers what it holds rather than a log a new reader can rewind.

If you want none of that, a subscription's default, AtMostOnce with no DurableName, is exactly what this section describes and costs nothing to ask for.

No delivery guarantee beyond what a subscription asks for and a backend declares

The default, AtMostOnce, promises one attempt and that a failure is counted, not that delivery happened. AtLeastOnce is the stronger mode this module offers: a subscription names a DurableName, the backend retries until a handler settles it with Ack or Nack, and a mode the backend has not declared is refused at construction rather than silently downgraded. See settle a delivery for what a handler owes under each mode, and the backend contract for what a backend has to declare to offer it.

What no mode promises, ever, is exactly-once: a redelivered message is a duplicate a handler has to tolerate, under both SlowHandler policies, which is why an at-least-once handler must be idempotent or safe under concurrent execution of the same message.

No ordering across subscribers

Each subscriber sees its own messages in order. Nothing coordinates two subscribers with each other.

And per-subscriber ordering holds only at Concurrency: 1. A subscription with more than one reader surrenders it, deliberately and at your choosing at wiring time: Concurrency: 1 keeps ordering, anything above it does not, and there is no runtime scaling that could take it away without you asking. See scale a subscription with concurrency.

No universal broker abstraction

Spanning SQS, Kafka and JMS is explicitly not the goal. The intersection of every broker is "publish bytes to a name", and an abstraction built to that is not worth having.

What is claimed instead is considerably less: two delivery shapes that were measured portable, and everything beyond them declared as a capability and discovered rather than assumed. Four provider modules hold to the same conformance suite this module ships: messaging-nats, messaging-sqs, messaging-rabbitmq and messaging-amqp. That is what tests an abstraction shaped by one implementation: a queue with no wildcard subscription and no fan-out without a second service found gaps a channel-backed backend could not, because in-process channels can do everything core NATS can minus the network.

No real-time media streams

Audio and video frames do not belong here, and the reasons are three decided properties rather than a performance worry:

  • the unit is a CloudEvent, so a frame is wrapped per frame, per speaker, per source — and on a fan-out subject the payload is copied again per subscriber, in CopyEvent, so two subscribers cannot see each other's mutations;
  • a shed is silent to the publisher under the default at-most-once mode, and media dropped at a full queue cannot be re-requested the way AtLeastOnce retries a message a handler declined;
  • per-subscriber ordering survives only at Concurrency: 1, so reconstructing a stream from more than one reader means reordering it yourself, which is exactly the cost this module refuses to hide behind a guarantee it cannot keep.

Those are the same three properties that make the bus good at what it is for. A component wanting the opposite of all three wants a different component.

Two of those three used to be stated more strongly than they deserved, and the corrections are worth keeping rather than quietly folding in.

The copy is ours, not the envelope's. This page used to say a CloudEvent forces a copy per frame. It does not — cloudevents.Marshal aliases Data deliberately, because a codec on a message hot path should not double every payload. The copy is real, but it is CopyEvent and it happens once per subscriber, which makes this a weaker objection for a single competing consumer than it reads.

Silent means silent to the publisher. A shed is not unrecorded: it is counted and attributed to the subscription that fell behind, which is this module's one actual guarantee. For a consumer whose requirement is counted, not prevented, that distinction is the whole answer.

The refusal stands on the third property, and on the second read correctly. It should not have stood on the first.

Media is out; a reference to stored media is not. A pointer to a segment already written to object storage, with a manifest, is an ordinary event: serialisable, low rate, and recoverable rather than merely tolerable when one is shed, because the segment is durable and the reference can be rebuilt. What is refused is putting the bytes on the bus, not knowing about them.

No circuit breaker, no health-based routing, no backoff on redelivery

Nothing here reroutes traffic away from a struggling dependency, a degraded competing-consumer member, or a handler that keeps declining. RoutesAroundDegraded names the middle one as a capability precisely because it is almost always false: "queue group" suggests load balancing, and NATS distributes rather than balances, so a wedged member keeps receiving its share, fills, and sheds while a healthy sibling idles.

Under AtLeastOnce, MaxDeliver bounds how many times a message is retried, but redelivery is immediate, with no backoff yet: a handler that nacks a transient failure burns its whole budget in milliseconds, which is useless for the failure it exists to survive. Until backoff lands, a handler that expects a dependency to recover should absorb the wait itself before it nacks, or size MaxDeliver for the retries it actually wants. See settle a delivery.

What this module does offer instead is supervision at the subscription boundary: a subscription whose readers all fail is quarantined and, under its RestartPolicy, restarted on a backoff of its own. That is a property of the subscription surviving, not of one message's delivery being retried intelligently, and the two are easy to conflate and answer different questions.

No configuration library

This module takes typed settings. The calling application maps its configuration onto them, and secrets arrive as a credential source rather than a string. A toolkit module that reads configuration decides for its consumer where configuration comes from.