nats-io/nats-server · error
expected consumer, found %q
Error message
expected consumer, found %q
What it means
The snapshot archive must list consumer entries as 'consumers/<name>' in the order declared by nstate.Consumers. While reading the expected consumer entries, if an archive entry's name lacks the 'consumers/' prefix, the restore aborts — the archive layout does not match the snapshot contract. Note: this loop also runs when nstate.Consumers == 0, in which case tr.Next() may return an end sentinel handled separately.
Source
Thrown at server/stream_backup.go:418
o.switchToEphemeral()
}
for _, o := range restoredConsumers {
if err := o.completeRestore(); err != nil {
if err = fmt.Errorf("failed to activate consumer %q: %w", o.name, err); retErr == nil {
retErr = err
}
s.Warnf("JetStream stream restore for '%s > %s' failed to activate consumers: %v", a.Name, cfg.Name, err)
}
}
}()
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 haveView on GitHub (pinned to 3a66a489d2)
Solutions
- Re-create the snapshot with nats stream backup / StreamSnapshot so entry names follow the consumers/<name> convention
- Inspect the archive entries (decompress with s2, list tar headers) and confirm each consumer entry starts with 'consumers/'
- Ensure state.json's Consumers count matches the number of consumers/<name> entries in the archive
- If repacking manually, keep exact entry names and ordering: state.json, then each consumers/<name> entry, then messages
Example fix
// before: repacked archive entry named 'consumer_orders' // after: correct snapshot layout entry name "consumers/orders"
Defensive patterns
Strategy: validation
Validate before calling
func checkConsumerEntries(r io.ReadSeeker, want int) error {
tr := archive.NewReader(s2.NewReader(r))
if h, err := tr.Next(); err != nil || h.Name != "state.json" {
return fmt.Errorf("not a snapshot archive")
}
io.Copy(io.Discard, tr)
for i := 0; i < want; i++ {
h, err := tr.Next()
if err != nil || !strings.HasPrefix(h.Name, "consumers/") {
return fmt.Errorf("entry %d not a consumer entry", i)
}
io.Copy(io.Discard, tr)
}
return nil
} Try / catch
_, err := acc.RestoreStreamV2(cfg, r)
if err != nil {
if strings.Contains(err.Error(), "expected consumer, found") {
// archive layout mismatch: regenerate snapshot with nats stream backup
}
return err
} Prevention
- Use nats stream backup / StreamSnapshot to build archives; don't repack by hand
- Keep the consumers/<name> entry naming and count matching state.json's Consumers
- Never mix snapshot formats across backup tool versions
- Validate archive entry names before restoring
When it happens
Trigger: A snapshot archive whose entries were renamed/reordered by external tooling (tar re-pack dropping the consumers/ prefix), a v1-format or custom backup lacking the consumers/ naming, or state.json declaring more consumers than the archive actually contains so the reader lands on a non-consumer entry (e.g. a message chunk or sentinel).
Common situations: Hand-repacking a backup with tar and losing the expected entry names; mixing backup formats between nats-server versions; a truncated archive where the message section bleeds into where consumer entries were expected; custom export pipelines generating wrong entry names.
Related errors
- expected state.json first
- failed to activate consumer %q: %w
- failed to read consumer %q state: %w
- failed to decode consumer %q state: %w
- max_request_batch must be set if it's JetStream limits are s
AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02).
Data as JSON: /api/errors/7864b94d0c836ac6.
Report an issue: GitHub.