nats-io/nats-server · error

failed to decode consumer %q state: %w

Error message

failed to decode consumer %q state: %w

What it means

A consumer entry's bytes were read successfully but could not be decoded as JSON into SnapshotConsumerState; the json.Unmarshal error is wrapped via %w. The consumer state in the snapshot is syntactically broken or from an incompatible schema.

Source

Thrown at server/stream_backup.go:426

			}
		}
	}()
	for range nstate.Consumers {
		hdr, err := tr.Next()
		if err != nil {
			return nil, err
		}
		name, found := strings.CutPrefix(hdr.Name, "consumers/")
		if !found {
			return nil, fmt.Errorf("expected consumer, found %q", hdr.Name)
		}
		buf, err := io.ReadAll(tr)
		if err != nil {
			return nil, fmt.Errorf("failed to read consumer %q state: %w", name, err)
		}
		var consumer SnapshotConsumerState
		if err := json.Unmarshal(buf, &consumer); err != nil {
			return nil, fmt.Errorf("failed to decode consumer %q state: %w", name, err)
		}
		if consumer.ConsumerConfig == nil {
			return nil, fmt.Errorf("consumer %q is missing config", name)
		}
		if consumer.ConsumerState == nil {
			return nil, fmt.Errorf("consumer %q is missing state", name)
		}
		isEphemeral := !isDurableConsumer(consumer.ConsumerConfig)
		if isEphemeral {
			// Keep ephemerals alive and interested until all messages have
			// been restored, then start their normal inactivity lifecycle.
			consumer.Durable = name
		}
		o, err := mset.addConsumerForRestore(consumer.ConsumerConfig)
		if err != nil {
			return nil, fmt.Errorf("failed to add consumer %q: %w", name, err)
		}
		if isEphemeral {

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Read the wrapped JSON error to find the offending field and fix the consumer entry's JSON to match SnapshotConsumerState (ConsumerConfig + ConsumerState objects present)
  2. Regenerate the snapshot from the source stream rather than hand-crafting consumer files
  3. Align nats-server versions between the backup and restore hosts to eliminate schema drift
  4. Validate offline: extract the consumers/<name> entry and run it through a JSON parser / unmarshal into SnapshotConsumerState before restoring

Example fix

// before: consumer entry missing required sections
{"Durable": "orders"}
// after: full SnapshotConsumerState shape
{"ConsumerConfig": {"Durable": "orders", "AckPolicy": "explicit"}, "ConsumerState": {"DeliveredConsumerSeq": 1, "AckFloorConsumerSeq": 1}}
Defensive patterns

Strategy: validation

Validate before calling

var cs SnapshotConsumerState
if err := json.Unmarshal(rawConsumerJSON, &cs); err != nil {
    return fmt.Errorf("consumer state invalid: %w", err)
}
if cs.ConsumerConfig == nil || cs.ConsumerState == nil {
    return fmt.Errorf("consumer entry missing config/state")
}

Try / catch

_, err := acc.RestoreStreamV2(cfg, r)
if err != nil {
    if strings.Contains(err.Error(), "failed to decode consumer") {
        // locate the offending consumers/<name> entry via the error and fix or drop it
    }
    return err
}

Prevention

When it happens

Trigger: json.Unmarshal(buf, &SnapshotConsumerState) fails: the consumer file inside the archive was hand-edited, produced by a different nats-server version with a changed consumer-state schema, or is a placeholder/garbage file placed at a consumers/<name> path by a custom pipeline.

Common situations: Manual edits to consumer state (deliver/ack floor sequences) introducing invalid JSON or wrong types; restoring snapshots across major version gaps; backup tooling that wrote non-JSON placeholders; file corruption localized to one entry.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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