Lesson 4 · Core NATS & the JetStream model

Consumers

The reader's cursor into a stream — push or pull, durable or not — and the one call your services use to create it.

Your win: explain what a consumer is, choose between push/pull and durable/ephemeral, and read a real QueueSubscribe from your own service.

A consumer is a stateful cursor

A stream just stores messages. A consumer is how a subscriber reads them: a stateful cursor that tracks its position in the stream and which messages are still awaiting acknowledgement.1 Crucially, one stream can have many consumers, each at its own position — so conversationmgmt and notification can each read the same events independently, at their own pace. Stream = the log; consumer = your bookmark in it.

The three choices that define a consumer

Push vs pull

Push: the server delivers messages to you as they arrive. Pull: you Fetch batches on your own schedule, controlling flow. This repo is push everywhere — except one pull consumer (activity-log, Lesson 11).

Durable vs ephemeral

Durable: named, survives disconnects — a restarted service resumes where it left off. Ephemeral: vanishes when the subscriber disconnects. The repo's consumers are all durable (Durable… names).

Deliver policy

Where to start reading: DeliverNew (only messages after creation — the repo default, so a first deploy doesn't replay history), DeliverAll (from the start), DeliverLast, by time/sequence.

A real consumer: conversationmgmt's User.Created

Your services create their consumers by calling QueueSubscribe. Here's the real one that reacts to a new user:

internal/conversationmgmt/modules/agora_usermgmt/controller/nats/user_created_subscriber.go:28-45 (condensed)
jsm.QueueSubscribe(
    constants.SubjectUserCreated,                   // "User.Created" — the subject to receive
    constants.QueueConversationMgmtUserCreated,     // the queue group (load-balance replicas)
    nats.Option{ JetStreamOptions: []JSSubOption{
        nats.Bind(constants.StreamUser, constants.DurableConversationMgmtUserCreated),  // stream + durable name
        nats.DeliverSubject(constants.DeliverConversationMgmtUserCreated),  // the push deliver subject
        nats.ManualAck(),                           // we ack ourselves (at-least-once)
        nats.MaxDeliver(3),                         // retry up to 3× then give up
        nats.AckWait(30 * time.Second),             // redeliver 30s after a no-ack
        nats.MaxAckPending(30),                     // ≤30 unacked in flight (backpressure)
    }},
    handler,                                        // creates the user in the Agora chat vendor
)

Read it as a sentence: "on subject User.Created, as a load-balanced group, bound to the user stream with a durable named cursor, ack manually, retry up to 3 times 30 s apart, at most 30 in flight." Every subscriber in the repo is a variation on this one shape — the UserGroup.Upserted one adds DeliverNew() and SkipMsgOlderThan and bumps to MaxDeliver(10) / AckWait(300s) (Lesson 4's deliver-policy card).

The golib provisions the consumer for you Before it subscribes, the golib's QueueSubscribe forces AckExplicit and calls UpsertConsumer (with a 5-attempt retry) to create-or-reconcile the durable consumer on the server — an idempotent upsert that deletes and recreates the consumer if its config drifted (Lesson 8 details the ack side). So you declare the consumer's config in the subscribe options, and the golib makes the server match — the same git-as-config discipline you saw in the CDC course, applied to consumers.
Stream vs consumer — nail this distinction A stream is shared, durable storage for subjects (provisioned by fink, Lesson 3). A consumer is one subscriber's cursor into it (provisioned at subscribe time, with its own ack/redelivery/filter config). Many consumers per stream; each independent. If you only remember one thing from Part 1: the stream stores, the consumer reads.
Read this next

JetStream consumers

Push vs pull, durable vs ephemeral, deliver & ack policies — the config you just read.

docs.nats.io — Consumers
Consumer details (ack policy, AckWait, MaxDeliver)

Check yourself (from memory)

Q1. What is a consumer, relative to a stream?

A consumer tracks position + pending acks; many consumers can read one stream independently, each at its own position.

Q2. What does a durable consumer give you?

Durable = named + survives disconnects, so a restarted service picks up where it left off. Ephemeral vanishes on disconnect.

Q3. Why do the repo's consumers use DeliverNew()?

DeliverNew = begin at messages published after the consumer is created — no surprise reprocessing of the stream's backlog.
Recall: consumers — the three choices + the QueueSubscribe shape.
cursor + 3 choices + the call, then reveal
A consumer = a stateful cursor into a stream (position + pending acks); many per stream, each independent. Three choices: push (server delivers) vs pull (client Fetch — repo is push except zeus activity-log); durable (named, resumes after restart) vs ephemeral (repo = durable); deliver policy DeliverNew (repo default — no history replay) / All / Last. The call (QueueSubscribe): subject + queue group + options Bind(stream, durable), DeliverNew, ManualAck, MaxDeliver(3), AckWait(30s), MaxAckPending(30), handler. The golib forces AckExplicit + calls UpsertConsumer (idempotent, delete+recreate on drift). Stream stores; consumer reads.
🏁 Part 1 complete — core NATS & the JetStream model You can now explain the whole model: core NATS vs JetStream and NATS vs Kafka (L1), subjects, wildcards, and queue groups (L2), streams and interest retention (L3), and consumers — push/pull, durable, ack policy — with a real QueueSubscribe (L4). You can read the shape of any subscriber. Part 2 opens the golib that makes it run.
🎯 Interview one-liner "How does a consumer work in JetStream?" → "It's a durable, stateful cursor into a stream — many consumers can read the same stream at their own positions. We use push queue-group consumers almost everywhere: bound to a stream with a durable name, starting at new messages, explicit manual ack, with a redelivery cap and an ack-wait timeout. The consumer config is declared at subscribe time and reconciled onto the server idempotently."
Part 2 opens the golib wrapper — the connection, publishing with the tenancy-carrying envelope, the consumer handler where RLS survives the async hop, and delivery semantics. Tell me "build Part 2" when you're ready, or ask me anything about consumers first. Re-take these four quizzes tomorrow cold — spacing is what makes it stick.

1. docs.nats.io — Consumers. In-repo: …/agora_usermgmt/controller/nats/user_created_subscriber.go:28-45, jetstream.go:583-623.