Lesson 8 · Publishing, consuming & delivery

Delivery semantics

What "at-least-once" actually buys you here — the ack model, how a failure redelivers, and the two guardrails this repo deliberately doesn't use.

Your win: explain how explicit acks make delivery at-least-once, name the four ack replies, describe exactly what this repo does on success/failure/poison — and name the gaps.

At-least-once = explicit ack

JetStream gives at-least-once delivery, and the mechanism is the acknowledgement. Every consumer here forces AckExplicit: a delivered message stays "pending" on the server until the consumer acks it. If the ack never comes (the handler failed, the pod crashed), the server redelivers.1 That's the whole guarantee: a message is delivered until someone confirms they handled it — so it can be delivered more than once, but never zero times.

The four ack replies (and the two this repo skips)

Vanilla JetStream gives a consumer four ways to reply to a message:2

ReplyMeaningUsed here?
Ackdone — advance, deleteyes
Nakcan't process now — redeliver soonno
Termnever processable — stop redeliveringno
InProgressstill working — reset the ack timerno
What the repo actually does (from handleMsg) This repo uses only Ack, and drives redelivery by timeout: So "retry" is "don't ack and wait," and "give up" is "ack to discard." Simpler, but you lose the immediate redelivery a Nak gives and the explicit intent of a Term.

The safety valves: AckWait, MaxDeliver, dedup

⏱ AckWait

How long the server waits for an ack before redelivering. Set per consumer (30s–300s here; 2–4s locally so dev doesn't hang). A retryable failure waits this long, then comes back.

🔁 MaxDeliver

The redelivery cap (3 or 10 here). After this many attempts the server gives up and drops the message. There is no DLQ — a message that exhausts MaxDeliver is surfaced only in metrics/logs.

🔑 Dedup + idempotency

Publish stamps a Nats-Msg-Id (Lesson 6) → JetStream's dedup window drops repeats. And consumers add app-level idempotency (notification's IdempotentService) so a redelivered message doesn't double-apply.

The pairing that makes at-least-once safe At-least-once means a message can arrive twice. The fix is the same idea you met in the CDC course: make the handler idempotent. Dedup by Nats-Msg-Id catches publish-side repeats; app-level idempotency (and idempotent writes) catches consumer-side redelivery. At-least-once delivery + idempotent handling = effectively-once result. That's the interview answer.
Two more guards, and the local overrides MaxAckPending caps in-flight unacked messages per consumer (30 → 100000) — backpressure so a slow handler can't be flooded. SkipMsgOlderThan (the learner services' aWeekAgo) skips-and-acks messages older than a threshold, so a replay storm after downtime doesn't reprocess ancient events. And isLocal shortens AckWait and drops to 1 replica so local dev never hangs waiting on redelivery.
Read this next

Acknowledgements & the delivery model

The four ack types, AckWait/MaxDeliver, and how at-least-once is achieved — the vocabulary to compare against what this repo does.

docs.nats.io — model deep dive (acks)
Consumers · in-repo internal/golibs/nats/jetstream.go:719-737

Check yourself (from memory)

Q1. On a retryable failure, what does the repo's handler do?

Redelivery is timeout-driven: don't ack → the server redelivers when AckWait expires. No explicit Nak.

Q2. What happens after a message hits MaxDeliver?

After MaxDeliver the message is dropped, surfaced only via metrics/logs. No DLQ — a real gap.

Q3. Why is at-least-once safe here despite possible duplicates?

At-least-once + idempotent handling = effectively-once. Dedup catches publish repeats; app-level idempotency catches redelivery.
Recall: delivery semantics — acks, redelivery, dedup, gaps.
at-least-once + ack model + guards + gaps, then reveal
At-least-once via forced AckExplicit: a message is pending until acked; no ack → redeliver. Four ack replies: Ack (done), Nak (redeliver now), Term (never), InProgress (reset timer) — repo uses ONLY Ack: success→Ack; retryable (retry=true)→no-ack→redeliver after AckWait; poison (retry=false/unmarshal-fail)→Ack (drop). Guards: AckWait (30–300s; 2–4s local) = redelivery timeout; MaxDeliver (3/10) = cap then drop — NO DLQ; Nats-Msg-Id dedup window + app-level idempotency → at-least-once + idempotent = effectively-once; MaxAckPending = backpressure; SkipMsgOlderThan = stale guard. Gaps: no Nak/Term, no DLQ.
🏁 Part 2 complete — publishing, consuming & delivery You can now explain the golib end to end: the lazy connection (L5), publishing with the tenancy envelope + dedup (L6), the handler where tenancy survives the hop (L7), and the delivery semantics that make it reliable — and honest about the gaps (L8). Part 3 puts all of it into your own services.
🎯 Interview one-liner "What delivery guarantee does your NATS eventing give?" → "At-least-once via explicit acks: a message redelivers until it's acked, capped by MaxDeliver. We drive retries by ack-wait timeout rather than explicit Nak, and we drop poison messages by acking them. There's no dead-letter queue — a gap. Safety comes from idempotency: a dedup id on publish plus idempotent consumers make at-least-once effectively-once."
Part 3 lands it all in your code — the conversationmgmt subscribers, the convo→notification publish link, the activity-log firehose, and NATS vs Kafka. Tell me "build Part 3" when you're ready, or ask me about the ack model first. Re-take Part 2's quizzes cold tomorrow.

1. docs.nats.io — Consumers (ack).

2. model deep dive — Ack/Nak/Term/InProgress. In-repo: internal/golibs/nats/jetstream.go:719-737.