nats-io/nats-server · error · JSStreamRollupFailedError

JS_STREAM_ROLLUP_FAILED

JS_STREAM_ROLLUP_FAILED

Error message

rollup not permitted

What it means

checkMsgHeadersPreClusteredProposal validates rollup headers on batched publishes. A rollup (Nats-Rollup: sub|all) is only allowed when the stream permits rollups (allowRollup, purge not denied, non-sourced msgs) and the subject hasn't already been rolled up in this batch; otherwise JS_STREAM_ROLLUP_FAILED 'rollup not permitted' is returned.

Source

Thrown at server/jetstream_batching.go:929

					var smv StoreMsg
					sm, _ := mset.store.LoadLastMsg(schedSubj, &smv)
					invalid = sm != nil && len(sliceHeader(JSSchedulePattern, sm.hdr)) == 0
				}
				if invalid {
					apiErr := NewJSMessageSchedulesSchedulerInvalidError()
					return hdr, msg, 0, apiErr, apiErr
				}
			}
		} else if !sourced && len(sliceHeader(JSScheduler, hdr)) > 0 {
			// Clients may only use Nats-Scheduler alongside Nats-Schedule-Next.
			apiErr := NewJSMessageSchedulesSchedulerInvalidError()
			return hdr, msg, 0, apiErr, apiErr
		}

		// Check for any rollups.
		if rollup := getRollup(hdr); rollup != _EMPTY_ {
			if (!allowRollup || denyPurge) && !sourced {
				err := errors.New("rollup not permitted")
				return hdr, msg, 0, NewJSStreamRollupFailedError(err), err
			}
			switch rollup {
			case JSMsgRollupSubject:
				// Rolling up the subject is only allowed if the first occurrence of this subject in the batch.
				if _, ok := diff.inflight[subject]; ok {
					err := errors.New("batch rollup sub invalid")
					return hdr, msg, 0, NewJSStreamRollupFailedError(err), err
				}
			case JSMsgRollupAll:
				// Rolling up the whole stream is only allowed if this is the first message of the batch.
				if len(diff.inflight) > 0 {
					err := errors.New("batch rollup all invalid")
					return hdr, msg, 0, NewJSStreamRollupFailedError(err), err
				}
			default:
				err := fmt.Errorf("rollup value invalid: %q", rollup)
				return hdr, msg, 0, NewJSStreamRollupFailedError(err), err

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Enable rollups on the stream: `nats stream update ORDERS --allow-rollup` or set AllowRollup: true in StreamConfig.
  2. Remove the Nats-Rollup header from messages published to streams that disallow rollups.
  3. Send at most one rollup per subject per batch; split further rollups into later batches.
  4. Check DenyPurge on the stream — rollups are denied when purging is denied unless the message is sourced.

Example fix

// before: stream without rollup permission
streamCfg := &nats.StreamConfig{Name: "ORDERS"} // AllowRollup defaults false
js.PublishMsg(&nats.Msg{Subject: "orders.new", Header: nats.Header{"Nats-Rollup": []string{"sub"}}, Data: data})
// -> JS_STREAM_ROLLUP_FAILED rollup not permitted
// after
streamCfg.AllowRollup = true
js.UpdateStream(streamCfg)
js.PublishMsg(&nats.Msg{Subject: "orders.new", Header: nats.Header{"Nats-Rollup": []string{"sub"}}, Data: data})
Defensive patterns

Strategy: validation

Validate before calling

si, err := js.StreamInfo("ORDERS")
if err == nil && !si.Config.AllowRollup {
	return fmt.Errorf("stream ORDERS disallows rollup; enable AllowRollup first")
}

Try / catch

_, err := js.PublishMsg(&nats.Msg{Subject: subj, Header: rollupHdr, Data: data})
var apiErr *nats.APIError
if errors.As(err, &apiErr) && apiErr.ErrorCode == nats.JSStreamRollupFailed {
	// enable rollup on the stream or republish without the header
}

Prevention

When it happens

Trigger: Publishing a rollup message to a stream with AllowRollup=false or DenyPurge=true; a second rollup for the same subject within one batch (diff.inflight collision).

Common situations: Streams created before rollup support or explicitly configured AllowRollup:false receiving rollup headers; compaction/aggregation jobs sending repeated rollups per subject in one batch; mirrored/restored streams with different policies.

Related errors


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