nats-io/nats-server · error

bad ack floor for stream

Error message

bad ack floor for stream

What it means

consumerFileStore.Update() validates that a consumer's ack floor never exceeds the last delivered sequence. If state.AckFloor.Stream > state.Delivered.Stream, the incoming ConsumerState is internally inconsistent — messages cannot have been acknowledged past the point they were delivered. This is a sanity check in the JetStream file-based consumer state store (server/filestore.go).

Source

Thrown at server/filestore.go:13874

func (o *consumerFileStore) UpdateConfig(cfg *ConsumerConfig) error {
	o.mu.Lock()
	defer o.mu.Unlock()

	// This is mostly unchecked here. We are assuming the upper layers have done sanity checking.
	csi := o.cfg
	csi.ConsumerConfig = *cfg

	return o.writeConsumerMeta()
}

func (o *consumerFileStore) Update(state *ConsumerState) error {
	// Sanity checks.
	if state.AckFloor.Consumer > state.Delivered.Consumer {
		return fmt.Errorf("bad ack floor for consumer")
	}
	if state.AckFloor.Stream > state.Delivered.Stream {
		return fmt.Errorf("bad ack floor for stream")
	}

	// Copy to our state.
	var pending map[uint64]*Pending
	var redelivered map[uint64]uint64
	if len(state.Pending) > 0 {
		pending = make(map[uint64]*Pending, len(state.Pending))
		for seq, p := range state.Pending {
			pending[seq] = &Pending{p.Sequence, p.Timestamp}
			if seq <= state.AckFloor.Stream || seq > state.Delivered.Stream {
				return fmt.Errorf("bad pending entry, sequence [%d] out of range", seq)
			}
		}
	}
	if len(state.Redelivered) > 0 {
		redelivered = make(map[uint64]uint64, len(state.Redelivered))
		for seq, dc := range state.Redelivered {
			redelivered[seq] = dc

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Check state.AckFloor.Stream vs state.Delivered.Stream before calling Update and correct or reject the state
  2. Use ForceUpdate() instead if the regression is intentional during recovery (it skips the backwards check)
  3. Regenerate the consumer state from the actual stream contents rather than trusting the imported snapshot
  4. Verify the stream's delivered sequence hasn't been reset (e.g. stream recreated with lower sequence)

Example fix

// before
if err := store.Update(&badState); err != nil { ... } // bad ack floor for stream
// after
if badState.AckFloor.Stream > badState.Delivered.Stream {
	badState.AckFloor = badState.Delivered
}
if err := store.Update(&badState); err != nil { ... }
Defensive patterns

Strategy: validation

Validate before calling

func validAckFloor(s *ConsumerState) bool {
	return s.AckFloor.Stream <= s.Delivered.Stream && s.AckFloor.Consumer <= s.Delivered.Consumer
}
if !validAckFloor(st) { return fmt.Errorf("refusing Update: ack floor ahead of delivered") }

Type guard

func stateIsConsistent(s *ConsumerState) bool {
	return s != nil && s.AckFloor.Stream <= s.Delivered.Stream
}

Try / catch

if err := store.Update(st); err != nil {
	if strings.Contains(err.Error(), "bad ack floor") {
		// rebuild state from stream instead of retrying
	}
	return err
}

Prevention

When it happens

Trigger: Calling Update(state *ConsumerState) with a state where AckFloor.Stream is greater than Delivered.Stream, typically from corrupted state, a restored snapshot from an older/rolled-back stream, or application code that advances the ack floor without advancing delivered.

Common situations: Restoring a consumer from a state file captured at a different point in time than the stream; manual state migration between servers; a bug in an operator tool that fabricates ConsumerState; crash-recovery mixing old ack state with new delivered state.

Related errors


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