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

  1. 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
  2. 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
  3. If pure sequence correctness (not atomicity) is required, drop the header and rely on Nats-Msg-Id dedupe instead
  4. 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

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


AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02). Data as JSON: /api/errors/a6da51da4dc1d572. Report an issue: GitHub.