nats-io/nats-server · error · JSStreamWrongLastSequenceError
10071
10071
Error message
last sequence mismatch: %d vs %d
What it means
The publish carried an 'Nats-Expected-Last-Sequence' header whose value does not match the stream's last committed sequence (clseq minus any failed sequences, clfs). nats-server rejects the message with JSStreamWrongLastSequenceError (API error 10071) to enforce optimistic-concurrency publish (dedupe/last-sequence guarantee) before a clustered proposal is made. The message includes both the client-supplied and server-side expected values.
Source
Thrown at server/jetstream_batching.go:751
// Keep the in-memory counters up-to-date.
if counter == nil {
counter = &msgCounterRunningTotal{}
}
counter.total = &initial
counter.sources = sources
counter.ops++
if diff.counter == nil {
diff.counter = map[string]*msgCounterRunningTotal{subject: counter}
} else {
diff.counter[subject] = counter
}
}
if len(hdr) > 0 {
// Expected last sequence.
if seq, exists := getExpectedLastSeq(hdr); exists && seq != mset.clseq-mset.clfs {
mlseq := mset.clseq - mset.clfs
err := fmt.Errorf("last sequence mismatch: %d vs %d", seq, mlseq)
return hdr, msg, 0, NewJSStreamWrongLastSequenceError(mlseq), err
} else if exists && len(diff.inflight) > 0 {
// Only the first message in a batch can contain an expected last sequence.
err := fmt.Errorf("last sequence mismatch")
return hdr, msg, 0, NewJSStreamWrongLastSequenceConstantError(), err
}
// Expected last sequence per subject.
if seq, exists := getExpectedLastSeqPerSubject(hdr); exists {
// Allow override of the subject used for the check.
seqSubj := subject
if optSubj := getExpectedLastSeqPerSubjectForSubject(hdr); optSubj != _EMPTY_ {
seqSubj = copyString(optSubj)
}
// The subject is already written to in this batch, we can't allow
// expected checks since they would be incorrect.
if _, ok := diff.inflight[seqSubj]; ok {View on GitHub (pinned to 3a66a489d2)
Solutions
- Fetch the actual last sequence (stream info, or use the per-subject variant) and retry the publish with the corrected Nats-Expected-Last-Sequence value
- Treat the returned JSStreamWrongLastSequenceError as a retryable conflict: re-read last sequence and republish with backoff, ideally with jitter to avoid livelock among competing publishers
- If pure sequence correctness (not atomicity) is required, drop the header and rely on Nats-Msg-Id dedupe instead
- Check for a purge/rollup or mirror/source that reset stream sequences and re-sync client state
Example fix
// before
js.PublishMsg(&nats.Msg{Subject: subj, Data: data, Header: nats.Header{"Nats-Expected-Last-Sequence": []string{"42"}}})
// after
for {
si, _ := js.StreamInfo(streamName)
last := si.State.LastSeq
_, err := js.PublishMsg(&nats.Msg{Subject: subj, Data: data,
Header: nats.Header{"Nats-Expected-Last-Sequence": []string{fmt.Sprint(last)}}})
if errors.Is(err, nats.ErrWrongLastSequence) { continue } // conflict: re-read and retry
break
} Defensive patterns
Strategy: retry
Validate before calling
si, _ := js.StreamInfo(streamName)
if si.State.LastSeq != expectedLastSeq {
// refresh expectedLastSeq before publishing
}
Type guard
func isWrongLastSeq(err error) (uint64, bool) {
var ae *nats.APIError
if errors.As(err, &ae) && ae.ErrorCode() == 10071 {
return ae.LastSequence, true
}
return 0, false
}
Try / catch
_, err := js.PublishMsg(msg)
var ae *nats.APIError
if errors.As(err, &ae) && ae.ErrorCode() == 10071 {
// server returned actual last sequence: resync and retry with backoff
}
Prevention
- Always source the expected sequence from the previous PubAck, not from a long-lived cache
- Use Nats-Msg-Id dedupe when atomicity per message is enough
- Add bounded retry with jitter on 10071 conflicts
- Watch for stream purges/rollups that reset sequences and invalidate client state
When it happens
Trigger: Publishing with Nats-Expected-Last-Sequence set when: another publisher appended messages between the client's read of the sequence and its publish; the client cached a stale sequence from a previous response; publish races on a stream that also receives messages via mirrors/sources or JS API; sequence computed manually instead of from a PubAck.
Common situations: Optimistic concurrency (idempotent/atomic writes) on busy streams where concurrent publishers frequently win the race; clients replaying a saved sequence after reconnect; per-subject expectation used where global expected-last-sequence was needed; stream purge/rollup reset the sequence but the client kept the old value.
Related errors
- 10164
- JetStream message size exceeds limits for '%s > %s'
- got corrupted escaped character
- unsupported BER encoding
- incomplete type, value pair
AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02).
Data as JSON: /api/errors/a6da51da4dc1d572.
Report an issue: GitHub.