Queue-Group Subscription

Table of Contents

This page defines the NATS pattern Queue-Group Subscription in the nats pattern language. It is the mechanism Competing Consumers takes over NATS. A service subscribes with `queue_subscribe()` and the queue group is the service name.

1. Also known as

2. Context

A service runs as one or more replicas behind a load balancer that knows nothing about NATS. Its subjects are shared by every replica and the bus delivers to whichever subscription matches.

3. Problem

How do several replicas share one subject so that each message is handled once, not once per replica?

4. Forces

  • A write must happen once, however many replicas are running.
  • A replica may be added for capacity with no change to any caller.
  • A replica may die while it holds a message.
  • A caller cannot know which replica will answer, and must not need to.

5. Solution

Subscribe with `queue_subscribe()`, with the service's own name as the queue group. The server delivers each message to exactly one member of the group:

namespace {
constexpr std::string_view queue_group = "ores.iam.service";
}

subs.push_back(nats.queue_subscribe(
    list_accounts_request::nats_subject,
    queue_group,
    [h](ores::nats::message msg) { h->list_accounts(msg); }));

The rule is total for a handler: a subject the service answers is a queue subscription, and the group is the service name. Three kinds of reader are deliberately not queue-subscribed, and each names its reason:

Reader Why not a queue group
A broadcast every replica must see, such as a work-available nudge or a worker heartbeat The point is that every peer receives it
A per-instance cache invalidator Each instance keeps its own copy, so each must be told
A client watching a subject, or a test observing a pipeline It is not a service serving work

A request/reply subject is never in that list: every replica answering the same request is a defect, because the caller reads one reply.

6. Failure handling

Concern What the pattern does
Errors A member that fails replies with the failure; the others are unaffected.
Timeouts Core NATS delivers at most once, so a member that dies mid-message loses it. Use Durable Subscription where that matters.
Retries A caller's retry re-sends to the group, so any member may serve it.
Idempotency Unaffected by the group itself; see Idempotent Message Identity.
Observability Each member logs the prefixed subject, so the handler is found by subject rather than by replica.

7. Pitfalls

  • Two queue-group names for one service. Each group receives every message and the work runs twice.
  • A plain `subscribe()` for a subject the service answers. Every replica handles every message, which for a request/reply subject means the caller receives whichever reply arrives first.
  • State in one replica's memory. The next message may land on another replica, which does not have it.

8. Security

Every member verifies the request context on every message; membership of the group grants no identity. See Identity and Access.

9. Resulting context

Benefits
Capacity grows by adding a replica, and a caller is unchanged.
Liabilities
No ordering, and per-replica state diverges.

10. In ORE Studio

The generated per-entity registrar declares the group as a constant and pushes each subscription into a `std::vector<subscription>`; the hand-written registrars follow the same shape. The convention is stated on `client::queue_subscribe()`.

11. Related patterns

12. Sources

13. See also

  • NATS service patterns — the structure note that orders this pattern language, and where to read this page in it.

Emacs 29.3 (Org mode 9.6.15)