A consumer whose last delivered sequence is below the stream’s first sequence has a cursor that references messages the stream no longer stores. This most commonly happens right after a stream purge, after retention (max_msgs, max_bytes, max_age) evicts old messages faster than the consumer processed them, or after a subject-filtered purge removes messages the consumer had queued up. It looks alarming in monitoring, but on its own it is not a stall — a JetStream consumer silently skips forward over messages that no longer exist and resumes delivery from the stream’s current first sequence.
The check surfaces a state, not a fault. It tells you the consumer’s delivered position has fallen behind the stream’s floor. That’s a useful operational signal — it tells you either the stream was purged/truncated while the consumer had unprocessed messages in the gap, or the consumer was lagging far enough that retention overtook it. Both are worth knowing about because they can imply message loss for that consumer (any messages inside the gap between the old delivered sequence and the new first_seq were removed before delivery).
What this check does not indicate, on its own, is that the consumer is stuck. This is a common misconception and it’s worth being explicit about it:
first_seq on its next delivery attempt and continues from there.If you are seeing an actual stall, treat this check as context — it tells you the stream floor moved — and then look at the other consumer health signals below. If the consumer’s delivered and ack_floor are ahead of the stream’s last_seq, that is the truly dangerous inverse case (CONSUMER_005) and is where stalls actually happen.
Stream purge. An operator ran nats stream purge (or the JS API equivalent) to clear a stream. The stream’s first_seq jumps forward past every existing consumer’s delivered cursor. Any messages that had been published but not yet delivered to a consumer are gone. The server snaps each consumer’s cursor forward as part of the purge, so after a full purge the delivered position sits exactly one below the new first_seq until the next message is delivered — on an idle stream the check keeps firing with a gap of 1 even though nothing is wrong.
Retention eviction (max_msgs, max_bytes, max_age). The stream’s own retention policy trimmed old messages. If a consumer was behind — perhaps due to a processing outage, a slow subscriber, or an ack pending buildup — retention advanced past its delivered position. The consumer was slow and the stream moved on without it.
Subject-filtered purge. nats stream purge --subject "orders.>" removes only messages matching the filter. A consumer subscribed to that filter whose delivered position was inside the purged range now has a cursor below the stream’s floor for its filtered view.
Restore from an older backup. A stream restored from a snapshot older than the consumer’s last delivery leaves the consumer’s cursor above/below the restored state depending on which direction the restore went. Below-first-seq is the harmless direction; above-last-seq is the dangerous one.
Source-based stream migration. A stream migrated via a source gets renumbered — the new stream assigns its own sequences. (A promoted mirror keeps the origin’s numbering.) Consumers whose positions were captured against the original stream may reference sequences that don’t exist in the new stream.
# Check the stream's sequence rangenats stream info ORDERS --json | jq '{ first_seq: .state.first_seq, last_seq: .state.last_seq, messages: .state.messages}'
# Check the consumer's delivered positionnats consumer info ORDERS my-consumer --json | jq '{ stream_seq_delivered: .delivered.stream_seq, ack_floor_stream_seq: .ack_floor.stream_seq, num_pending: .num_pending, num_ack_pending: .num_ack_pending}'If stream_seq_delivered is less than the stream’s first_seq, the check has fired. Compare the gap to the stream’s message rate to understand how many messages (if any) were lost inside the gap. A gap of exactly 1 usually just means a purge left the stream empty; a large gap means retention overtook a lagging or idle consumer.
STREAM="ORDERS"FIRST_SEQ=$(nats stream info "$STREAM" --json | jq '.state.first_seq')
for consumer in $(nats consumer list "$STREAM" --names); do DELIVERED=$(nats consumer info "$STREAM" "$consumer" --json | jq '.delivered.stream_seq') if [ "$DELIVERED" -lt "$FIRST_SEQ" ]; then GAP=$((FIRST_SEQ - DELIVERED)) echo "BELOW: consumer=$consumer delivered=$DELIVERED first_seq=$FIRST_SEQ gap=$GAP" fidone1import (2 "fmt"3 "github.com/nats-io/nats.go"4)5
6func checkDeliveredBelowFirst(js nats.JetStreamContext, streamName string) error {7 stream, err := js.StreamInfo(streamName)8 if err != nil {9 return err10 }11 firstSeq := stream.State.FirstSeq12
13 for consumer := range js.ConsumerNames(streamName) {14 info, err := js.ConsumerInfo(streamName, consumer)15 if err != nil {16 continue17 }18 if info.Delivered.Stream < firstSeq {19 fmt.Printf("BELOW: stream=%s consumer=%s delivered=%d first_seq=%d gap=%d\n",20 streamName, consumer, info.Delivered.Stream, firstSeq,21 firstSeq-info.Delivered.Stream)22 }23 }24 return nil25}1import asyncio2import nats3
4async def check_delivered_below_first(stream_name: str):5 nc = await nats.connect()6 js = nc.jetstream()7
8 stream = await js.stream_info(stream_name)9 first_seq = stream.state.first_seq10
11 # consumers_info pages at 256 results; pass offset= on streams with more consumers.12 for info in await js.consumers_info(stream_name):13 if info.delivered.stream_seq < first_seq:14 print(f"BELOW: stream={stream_name} consumer={info.name} "15 f"delivered={info.delivered.stream_seq} first_seq={first_seq} "16 f"gap={first_seq - info.delivered.stream_seq}")17
18 await nc.close()19
20asyncio.run(check_delivered_below_first("ORDERS"))Because this state is not itself a stall, verify the consumer is moving. Sample num_pending and delivered.stream_seq twice, a few seconds apart:
nats consumer info ORDERS my-consumer --json | jq '{np: .num_pending, ds: .delivered.stream_seq}'sleep 10nats consumer info ORDERS my-consumer --json | jq '{np: .num_pending, ds: .delivered.stream_seq}'If delivered.stream_seq is advancing (and is now at or above the stream’s first_seq), the consumer has already skipped over the gap and is fine — no action needed. If it is completely static while the stream is receiving new messages, look elsewhere: no subscription interest, drained subscription, ack pending buildup, quorum loss, or an application-side crash loop. Those are the real causes of a stalled consumer, not this check.
For the majority of firings the correct response is investigate, then leave the consumer alone. Understand which cause listed above applied, decide whether any messages inside the gap needed to be processed, and move on. The consumer will continue from the stream’s current first_seq on its next delivery.
Only reset a consumer when you have a concrete reason to — for example, you want to reprocess everything the stream still has after a purge, or you’re recovering from a stream restore/migration and want the consumer to explicitly re-anchor.
NATS Server 2.14 added a consumer reset API that repositions an existing consumer’s cursor — no delete-and-recreate required. This is preferable to deleting the consumer, which drops its state (ack floor, redelivery counters, pending) and can cause the client to reprocess messages the application had already acknowledged.
# Make the stream's current first sequence the next message delivered.FIRST_SEQ=$(nats stream info ORDERS --json | jq '.state.first_seq')nats consumer reset ORDERS my-consumer --sequence "$FIRST_SEQ"Reset semantics worth knowing:
nats consumer reset with no --sequence rewinds to the consumer’s ack floor — it replays delivered-but-unacknowledged messages, not the whole stream.--sequence N makes sequence N the next message delivered; it can move the cursor backward or forward. To skip everything currently in the stream and take only future publishes, reset to the stream’s last_seq + 1.all, new, by_start_sequence, …) still cannot be changed after creation — nats consumer edit does not accept it and the server rejects deliver policy changes on update.If your server predates NATS Server 2.14 (no consumer reset API), or you specifically want a clean consumer with no history, then delete-and-recreate is available — but be aware it discards all consumer state (ack floor, redelivery counters, ack pending) and, if messages have survived the purge, may cause the application to reprocess them. Do it deliberately, not as a default fix.
The consumer is chronically behind. That is a throughput/backpressure problem, not a cursor problem. Look at:
num_ack_pending and num_redelivered on the consumer — is the application acking slowly, or failing and redelivering?MaxAckPending — is it too small and rate-limiting delivery?Fixing this at the cursor level (reset, delete/recreate) will only mask the problem until retention catches up again.
No. This is the most important thing to understand about this check. A JetStream consumer whose delivered cursor is below the stream’s first sequence does not sit and wait — it advances its cursor to the current first_seq on the next delivery and continues. Persistent silence from a consumer in this state is caused by something else (no interest, drained subscription, ack pending buildup, quorum loss, application crash loop) and should be diagnosed as such.
Because reaching this state usually implies that messages inside the gap were lost from that consumer’s point of view — they were removed by the purge or by retention before the consumer had a chance to see them. That’s worth surfacing even if the consumer itself is fine. It’s a signal about what happened to the stream, not a diagnosis of a broken consumer.
Yes, on NATS Server 2.14 and newer. nats consumer reset <stream> <consumer> --sequence N repositions the cursor on the existing consumer so sequence N is the next message delivered; with no sequence it rewinds to the ack floor. On older servers the cursor is effectively immutable without delete-and-recreate. Note that the delivery policy can never be changed with consumer edit — repositioning is the reset API’s job.
No. Delete-and-recreate is heavier than it looks: it discards ack floor, ack pending, and redelivery counters, and — if any messages survived the purge or retention event — it can cause the application to reprocess messages it had already acked. Use consumer reset (or leave the consumer alone entirely) unless you specifically want a clean slate.
No. Both maintain the same delivered/ack-floor state and both handle the “requested sequence no longer exists” case the same way — they skip forward. If messages remain at or above the stream’s first sequence, both deliver them as normal; if the stream is empty (say, right after a full purge), a pull request waits out its expiry empty-handed and a push consumer stays quiet until a new publish. Neither one stalls because of this condition.
If a consumer’s delivered or ack_floor is above the stream’s last_seq — meaning the consumer’s cursor references a sequence the stream hasn’t reached or no longer has — the consumer will not deliver anything until the stream catches up, which after most events (restore from older backup, mirror promotion with renumbering, Raft recovery with rollback) it never will. That is CONSUMER_005. Historically this was almost always a bug; since NATS Server 2.14 it can also be deliberate, produced by a consumer reset that fast-forwards the cursor. Either way, that check — not this one — is the one that means “silent stall.”
With 100+ always-on audit Checks from the NATS experts, Insights helps you find and fix problems before they become costly incidents.
No alert rules to write. No dashboards to maintain.
News and content from across the community