Skip to main content

Acknowledgment

The shipping consumer was created with AckPolicy=explicit. That choice means every message it delivers must be answered. A message is not done until the client says so.

This page covers the four responses a client can give — ack, nak, term ("give up on this one"), and in-progress ("still working") — and the server-side controls that decide what happens when an answer is late or never comes.

Why the server waits for an answer

A message is open from the moment the server delivers it until the consumer answers. The server keeps a copy on the pending list and starts a timer.

If the answer never comes, the server assumes the worker stopped and delivers the message again. This is the redelivery loop from the previous page. It has two parts.

The first part is the timer. Its length is AckWait, and it defaults to 30 seconds.

The second part is the answer itself. The answer takes one of four forms.

The four responses

A client answers a delivered message in exactly one of four ways.

ack: the acknowledgment. The work succeeded. The server removes the message from the pending list and never delivers it again. This is the answer you send when a message is handled.

nak: a negative acknowledgment. The work failed, redeliver this message. The server puts it back for another attempt. A plain nak asks for redelivery right away.

term: stop trying. This message can never be handled, so don't deliver it again to anyone. The server drops it from the pending list as it does for an ack, but the work was never done.

in-progress: still working. This is not a final answer. It resets the AckWait timer so a long job doesn't trip redelivery. The client then keeps going and answers for real later.

ack, nak, and term are final. Each one closes out a delivery. in-progress extends the timer instead.

The rest of this page takes the three non-trivial answers — nak, term, and the controls behind them — one at a time. The wire format of each answer is in Reference → Consumer API.

Negative ack with a delay

A plain nak redelivers immediately. That's rarely what you want.

A failure is often temporary. A service it calls is briefly down, a row is locked, or a rate limit is hit. Redelivering in the same instant fails again right away, and the message retries over and over with no pause.

To avoid that, nak with a delay. The client tells the server to redeliver the message but wait a given time first. The server holds the message for that delay, then puts it back.

#!/bin/bash
# Processing failed. Tell the server to redeliver this message, but back
# off first so the retry does not fire into the same transient error.
#
# The CLI naks with `nats consumer next --nak`. A plain nak asks for
# redelivery right away. To space out successive redeliveries, configure
# a growing delay on the consumer with --backoff; the server then waits
# longer before each new attempt.

# Give the shipping consumer a growing redelivery delay: 1s, then more,
# up to 30s, across 5 steps.
nats consumer edit ORDERS shipping \
--ack=explicit \
--backoff=linear \
--backoff-steps=5 \
--backoff-min=1s \
--backoff-max=30s

# Pull the next message and negatively acknowledge it.
# The server applies the backoff delay before redelivering.
nats consumer next ORDERS shipping --nak

A nak returns the message to the consumer, not to the worker that nak'd it. If several workers share one consumer, the redelivery can land on a different worker (see worker pool). Each nak also raises a nak advisory on $JS.EVENT.ADVISORY.CONSUMER.MSG_NAKED.ORDERS.shipping; its fields are in Reference → Nak advisory.

A delayed nak sets the wait one redelivery at a time, and the client chooses it. To have the server apply a delay schedule on its own, growing the wait with each attempt, set a backoff on the consumer. Backoff is covered below.

Term: the poison message path

Some failures aren't temporary. A message with a broken payload, or one that fails a check that will never pass, is a poison message. Redelivering it wastes attempts and holds up the messages behind it.

For these, the client answers term. The message leaves the pending list and the server never delivers it again, no matter how many attempts remain.

#!/bin/bash
# This message can never be processed (a poison message: malformed
# payload, a validation that will never pass). Terminate it so the
# server drops it from the pending list and never redelivers it,
# regardless of how many delivery attempts remain.

# Pull the next message and term it.
nats consumer next ORDERS shipping --term

After a term, the message is gone from this consumer but not from the stream. Term runs through the same path as an ack: the pending entry clears and the acknowledgment floor moves past the message, so it's never redelivered to this consumer. The message itself stays in the stream under the default Limits retention — other consumers still see it, and it ages out with the stream's limits like any other message. On a WorkQueue or Interest stream, where a handled message is removed, a term removes it just as an ack would; see Retention policies.

The difference from an ack is that the work never happened, so the server records the give-up. It publishes a terminated advisory on $JS.EVENT.ADVISORY.CONSUMER.MSG_TERMINATED.ORDERS.shipping, carrying the stream and consumer sequence, the delivery count, and an optional reason you can attach to the term. Watch it the way the pitfall below watches the max-deliveries advisory, so a terminated order_id doesn't disappear without a trace. The advisory's fields are in Reference → Terminated advisory.

Use term only when the code can tell that no future attempt will succeed. When in doubt, nak with a delay and let the delivery limit below decide.

The server controls

The four responses are the client's side. The server has two settings that work with them, both on the consumer.

AckWait is the timer. If a delivery is not ack'd, nak'd, or kept alive with in-progress before AckWait runs out, the server treats it as a silent failure and redelivers. The default is 30 seconds. Shorten it for fast work, lengthen it for slow work.

MaxDeliver is the limit on attempts. It caps how many times the server will deliver one message before giving up. The default is -1, which means no limit. A message can be redelivered forever.

These two cover the two ways a delivery can fail. AckWait handles the case where no answer arrives. MaxDeliver caps the case where a worker keeps sending a nak on the same message.

A timeout and a nak reach the same redelivery loop. Whichever one starts it, the backoff schedule below sets how long the server waits before the next attempt. Backoff applies to timeouts as well as naks.

Set both on the consumer with nats consumer edit:

#!/bin/bash
# Set the two server-side ack controls on the shipping consumer.
#
# --ack=explicit each message must be answered on its own
# --wait=10s AckWait: redeliver if no answer within 10 seconds
# --max-deliver=5 cap delivery attempts at 5 (default -1 = unlimited)
nats consumer edit ORDERS shipping \
--ack=explicit \
--wait=10s \
--max-deliver=5

# Read the controls back. Look for: Ack Wait, Maximum Deliveries.
nats consumer info ORDERS shipping

Read them back from nats consumer info ORDERS shipping:

Configuration:

Ack Policy: Explicit
Ack Wait: 10.00s
Maximum Deliveries: 5
Replay Policy: Instant
Max Ack Pending: 1,000

With --max-deliver=5, a message that fails five times stops being delivered. Without a term path, that message is dropped after the fifth attempt. With a term path, your code retires the poison message itself, before the limit is reached.

Backoff: a growing delay between attempts

A flat AckWait waits the same amount of time before every redelivery. A backoff makes that wait grow.

The server holds a list of delays, one per attempt: wait one second before the second delivery, five seconds before the third, 30 before the fourth. The wait between attempts grows each round instead of staying the same.

The CLI builds the list for you from a range:

nats consumer edit ORDERS shipping --backoff=linear --backoff-steps=5 --backoff-min=1s --backoff-max=30s

If the list has fewer entries than MaxDeliver allows, the server reuses the last entry for the remaining attempts.

The full set of backoff options is documented in Reference → Consumer Configuration. We use only a linear range here.

Ack policy: the other values

This page assumed explicit, the policy shipping was created with. AckPolicy has three more values.

explicit answers each message on its own. It's what shipping uses and the right default for work that must not be lost — everything on this page depends on it. none requires no answer at all: the server treats a message as done the moment it's delivered, so there's no pending list, no Ack Wait, and no redelivery, and nothing on this page applies. all lets one ack answer every earlier message too — cheaper, but it only fits a consumer that processes strictly in order, since acking message 10 also retires 1 through 9. A fourth value, flow_control, belongs to push consumers and paces how fast the server delivers; pull consumers like shipping don't need it.

The full set of ack policies is in Reference → Consumer API. This page uses explicit.

Pitfalls

Each response and control is simple on its own. Most traps come from how they combine.

A plain nak retries with no delay. A nak with no delay asks for redelivery in the same instant. A temporary failure then retries right away, fails again, and ties up one worker on one message. Don't send a bare nak for a temporary failure. Nak with a delay, or set a backoff on the consumer so the wait grows each round (covered above).

A poison message with no term path uses every attempt. Without term, a broken payload is nak'd over and over until MaxDeliver gives up, using the full set of attempts and holding up the messages behind it. When the code can tell no future attempt will succeed, answer term so the message leaves the pending list at once instead of working through the limit.

MaxDeliver drops a message with no dead-letter. When a message hits the delivery limit, the server removes it from the consumer's pending list and never delivers it again. The message stays in the stream, but the shipping consumer's normal output says nothing, so the drop is easy to miss. JetStream has no built-in dead-letter queue. You can still catch the drop: the server publishes a max-deliveries advisory on $JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.ORDERS.shipping the moment a message goes past its limit (Reference → Max-deliveries advisory). Subscribe to it so a poison order_id isn't dropped without notice:

#!/bin/bash
# A message that hits MaxDeliver leaves the consumer with no dead-letter
# queue. It is not lost from the stream, but the shipping consumer stops
# delivering it -- and nothing in the consumer's normal output says so.
#
# The drop is observable: the server publishes an advisory the moment a
# message exceeds its delivery limit. Watch for it so a poison message
# does not vanish unnoticed.

# Subscribe to the max-deliveries advisory for the shipping consumer.
# The server publishes here when a message (e.g. order ord_8w2k) is
# delivered MaxDeliver times without a final ack.
nats sub '$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.ORDERS.shipping'

# Or watch every JetStream advisory in one stream, including this one:
# nats events --js-advisory --no-srv-advisory

AckWait shorter than real processing time causes double work. If a job often takes longer than AckWait and the worker never sends in-progress, the server decides the worker stopped and redelivers a message that's still being handled, so two workers run the same order. Either raise AckWait to cover the slow case, or send in-progress to reset the timer while a long job runs (both covered above).

Where you are

The shipping consumer is unchanged in shape (still pull, still AckPolicy=explicit), but now you understand it fully. You know the four answers a client gives, and the two server controls, AckWait and MaxDeliver, that decide when a message comes back and when it stops. A poison message has a clear exit through term.

What's next

One worker pulls one order at a time. The next page covers the two ways a client drives a pull consumer — fetching a batch versus consuming a continuous flow — and when to reach for each.

See also