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
| Reply | Meaning | Used here? |
|---|---|---|
Ack | done — advance, delete | yes |
Nak | can't process now — redeliver soon | no |
Term | never processable — stop redelivering | no |
InProgress | still working — reset the ack timer | no |
handleMsg)
This repo uses only Ack, and drives redelivery by timeout:
- Success →
msg.Ack()(done). - Retryable failure (
retry=true) → do nothing → the server redelivers afterAckWaitexpires. (Instead of an explicitNak.) - Poison (
retry=false, or an unparseable envelope) →msg.Ack()to drop it. (Instead of an explicitTerm.)
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.
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.
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.
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?
AckWait expires. No explicit Nak.
Q2. What happens after a message hits MaxDeliver?
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?
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.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."
1. docs.nats.io — Consumers (ack).
2. model deep dive — Ack/Nak/Term/InProgress. In-repo: internal/golibs/nats/jetstream.go:719-737.