nats-io/nats-server · error

failed to decode consumer

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.

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.

Appendix: 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)