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
- NATS: queue group.
- The pattern language: Competing Consumers.
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
- Durable Subscription — the same group with persistence.
- Own the Subscription Handle — how the group's membership is kept alive and drained.
12. Sources
- NATS documentation, Queue Groups.
- Gregor Hohpe and Bobby Woolf, Enterprise Integration Patterns, Addison-Wesley, 2003, Competing Consumers.
13. See also
- NATS service patterns — the structure note that orders this pattern language, and where to read this page in it.