Appearance
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
MatrixActorsubscribehelper today. Callers needing it usetransport.subscribedirectly with NATS-specific options. JetStream consumer groups are documented instatic streamsbut are workstream material.
When consumer groups make sense
| Scenario | Why a consumer group fits |
|---|---|
| Worker pool processing job inbox | Each job goes to exactly one worker |
| Inference fan-out across replicas | Spread load, all replicas equivalent |
| Background reconciler in HA mode | Many warm; only one acts on each tick |
| Audit-stream processors | Multiple 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 receivesWorkers 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
- Events — pub/sub fan-out without queue groups.
- Streaming / sessions — multi-message exchanges; not a consumer-group case.
- Mount claims — provider-side load distribution.
- Instance claims — multiple registered providers vs queue groups.