nats-io/nats-server · error · JSStreamHeaderExceedsMaximumError

10097

10097

Error message

JetStream header size exceeds limits for '%s > %s'

What it means

JetStream message header size limit error (JSStreamHeaderExceedsMaximumError, code 10097). NATS encodes header lengths as uint16, so headers must fit in 65535 bytes; batches or counter messages with oversized headers are rejected pre-proposal to avoid downstream corruption.

Source

Thrown at server/jetstream_batching.go:551

	diff *batchStagedDiff, mset *stream, subject, rsubject string, hdr []byte, msg []byte, sourced bool, name string,
	jsa *jsAccount, allowRollup, denyPurge, allowTTL, allowMsgCounter, allowMsgSchedules bool,
	discard DiscardPolicy, discardNewPer bool, maxMsgSize int, maxMsgs int64, maxMsgsPer int64, maxBytes int64,
) ([]byte, []byte, uint64, *ApiError, error) {
	var incr *big.Int
	var hasSchedule bool

	// Do this before staging any proposal state. All clustered publish paths,
	// including atomic and fast batches, use this helper.
	if mset.store.Type() == FileStorage && isFileStoreMsgTooLarge(fileStoreMsgSize(subject, hdr, msg)) {
		return hdr, msg, 0, NewJSStreamStoreFailedError(ErrMsgTooLarge), ErrMsgTooLarge
	}

	// Some header checks must be checked pre proposal.
	if len(hdr) > 0 {
		// Since we encode header len as u16 make sure we do not exceed.
		// Again this works if it goes through but better to be pre-emptive.
		if len(hdr) > math.MaxUint16 {
			err := fmt.Errorf("JetStream header size exceeds limits for '%s > %s'", jsa.acc().Name, name)
			return hdr, msg, 0, NewJSStreamHeaderExceedsMaximumError(), err
		}
		// Counter increments.
		// Only supported on counter streams, and payload must be empty (if not coming from a source).
		var ok bool
		if incr, ok = getMessageIncr(hdr); !ok {
			apiErr := NewJSMessageIncrInvalidError()
			return hdr, msg, 0, apiErr, apiErr
		} else if incr != nil && !sourced {
			// Only do checks if the message isn't sourced. Otherwise, we need to store verbatim.
			if !allowMsgCounter {
				apiErr := NewJSMessageIncrDisabledError()
				return hdr, msg, 0, apiErr, apiErr
			} else if len(msg) > 0 {
				apiErr := NewJSMessageIncrPayloadError()
				return hdr, msg, 0, apiErr, apiErr
			} else {
				// Check for incompatible headers.

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Reduce header size: trim or consolidate user headers to stay under 65535 bytes
  2. Move large metadata into the message payload instead of headers
  3. Split oversized atomic/fast batches into smaller batches so per-message headers fit

Example fix

// before
hdr := nats.Header{}
for i := 0; i < 5000; i++ { hdr.Add(fmt.Sprintf("X-Trace-%d", i), bigValue) }
// after
if hdrEncoded := encodeHeaders(hdr); len(hdrEncoded) > math.MaxUint16 {
	hdr = pruneLargeHeaders(hdr) // keep under 65535 bytes
}
Defensive patterns

Strategy: validation

Validate before calling

const maxHeader = math.MaxUint16
if h := encodedHeaderLen(hdr); h > maxHeader {
	return fmt.Errorf("header size %d exceeds %d", h, maxHeader)
}

Type guard

func headersWithinLimit(hdr nats.Header) bool {
	return encodedHeaderLen(hdr) <= math.MaxUint16
}

Try / catch

_, err := js.PublishMsg(msg)
var he *nats.APIError
if errors.As(err, &he) && he.ErrorCode == 10097 {
	msg.Header = pruneHeaders(msg.Header)
	_, err = js.PublishMsg(msg)
}

Prevention

When it happens

Trigger: Publishing a message (or atomic/fast batch entry, including counter increment messages) to a JetStream stream whose headers total more than math.MaxUint16 (65535) bytes.

Common situations: Accumulating very large header sets (many Nats-Msg-Id dedup or user headers); batch protocols appending headers per entry until the u16 limit overflows; source/transform pipelines copying headers between messages.

Related errors


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