Skip to content

Consumer groups

A consumer group lets multiple subscribers share the work on a single subject: each message is delivered to exactly one member of the group, not to all members. Matrix exposes this through NATS queue groups for load-balancing inbound requests, and through JetStream consumer groups for streams.

Status: target state, partial. Pub/sub queue-group support exists at the NATS layer but is not exposed as a first-class option on the standard MatrixActor subscribe helper today. Callers needing it use transport.subscribe directly with NATS-specific options. JetStream consumer groups are documented in static streams but are workstream material.

When consumer groups make sense

ScenarioWhy a consumer group fits
Worker pool processing job inboxEach job goes to exactly one worker
Inference fan-out across replicasSpread load, all replicas equivalent
Background reconciler in HA modeMany warm; only one acts on each tick
Audit-stream processorsMultiple processors split the stream

Without a queue group, every subscriber receives every message — appropriate for events but wrong for jobs.

Pub/sub queue groups (NATS)

NATS supports queue groups natively: subscribers attach a queue name and NATS delivers each message to one subscriber per group. Two queue groups on the same subject means each message reaches one subscriber per group.

                                      ┌── worker-1 (queue 'inference')
publisher ──→ system.inference.$inbox ┤── worker-2 (queue 'inference')   ← one of these
                                      └── worker-3 (queue 'inference')
                                       
                                      ┌── auditor   (no queue)            ← also receives
                                      └── monitor    (no queue)            ← also receives

Workers in the inference queue group share the inbox load; non-grouped subscribers (auditor, monitor) still receive everything.

The pattern in code (using the underlying NATS client directly):

typescript
const natsClient = transport.getUnderlyingClient();
natsClient.subscribe('COM.NIMBLETEC.RICHARD-SANTOMAURO.system.inference.$inbox', {
  queue: 'inference',
  callback: (err, msg) => { /* handle */ },
});

MatrixActor.subscribe does not currently take a queue option. Actors that need queue groups for their $inbox either subscribe directly via getUnderlyingClient() or accept that load-balancing happens at the mount-claim layer (multiple providerRuntimeIds, caller picks one).

JetStream consumer groups

For durable streams (static streams declarations), JetStream provides consumer groups via shared consumers: multiple workers attach to the same named consumer and JetStream load-balances messages.

The static streams declaration shape:

typescript
static streams = {
  'market.trades': {
    description: '...',
    schema: { ... },
    retention: '30d',
    maxBytes: '10GB',
  }
};

The system-blackboard and system-observability packages own the JetStream wiring; the MatrixActor framework methods (publishStream, consumeStream per ACTOR-COMMUNICATION-CONTRACT.md) are the forward-looking API. Per-consumer-group settings (deliver policy, ack policy, max ack pending) are configured at consumer creation time, not in the static declaration.

Caller-side fan-out via providers

A pattern that avoids queue groups entirely: the registry returns multiple providers and the caller load-balances client-side.

typescript
const { providers } = await invoke('system.registry', 'registry.resolve', {
  logicalMount: 'system.inference',
});
const live = providers.filter(p => p.status === 'live');
const target = pickByPolicy(live);   // round-robin, by metadata, etc.

This works for request/reply (one specific provider answers); it does NOT distribute event consumption (every subscribed provider gets every event unless they're in a queue group).

Failure modes

  • No queue group set when needed. Two workers each handle every job, causing duplicate work. Fix: add a queue group to both subscriptions.
  • Queue group set when not needed. Auditor only sees a fraction of the events (whatever queue picked it). Fix: drop the queue option for pure observers.
  • Queue group across runtime restarts. A worker reconnecting under a new connection joins the group fresh; in-flight messages may redistribute. JetStream consumer groups have stronger ack semantics; ephemeral queue groups do not.

See also