nats-io/nats-server · error

direct can not be updated

Error message

direct can not be updated

What it means

The Direct flag classifies a consumer for its entire lifetime; the stream relies on this classification when walking its consumer list, so it can never change. checkNewConsumerConfig rejects the update when cfg.Direct != ncfg.Direct. This typically occurs when toggling between a normal (ack-based) consumer and a direct/ephemeral one under the same durable name.

Source

Thrown at server/consumer.go:2543

	o.rlimit = rate.NewLimiter(rl, burst)
}

// Check if new consumer config allowed vs old.
func (acc *Account) checkNewConsumerConfig(cfg, ncfg *ConsumerConfig) error {
	if reflect.DeepEqual(cfg, ncfg) {
		return nil
	}
	// Something different, so check since we only allow certain things to be updated.
	if cfg.DeliverPolicy != ncfg.DeliverPolicy {
		return errors.New("deliver policy can not be updated")
	}
	if cfg.MemoryStorage != ncfg.MemoryStorage {
		return errors.New("storage type can not be updated")
	}
	// Direct and Sourcing classify the consumer for its whole lifetime, which the
	// stream relies on when walking its consumer list, so they can not change.
	if cfg.Direct != ncfg.Direct {
		return errors.New("direct can not be updated")
	}
	if cfg.Sourcing != ncfg.Sourcing {
		return errors.New("sourcing can not be updated")
	}
	if cfg.OptStartSeq != ncfg.OptStartSeq {
		return errors.New("start sequence can not be updated")
	}
	if cfg.OptStartTime != nil && ncfg.OptStartTime != nil {
		// Both have start times set, compare them directly:
		if !cfg.OptStartTime.Equal(*ncfg.OptStartTime) {
			return errors.New("start time can not be updated")
		}
	} else if cfg.OptStartTime != nil || ncfg.OptStartTime != nil {
		// At least one start time is set and the other is not
		return errors.New("start time can not be updated")
	}
	if cfg.AckPolicy != ncfg.AckPolicy {
		return errors.New("ack policy can not be updated")

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Delete the consumer and recreate it with the desired Direct setting.
  2. Use a distinct durable/name for the direct variant.
  3. Match the existing consumer's Direct value in update requests.
  4. Discover the current config via ConsumerInfo before composing updates.

Example fix

// before
js.UpdateConsumer(stream, &nats.ConsumerConfig{Durable: "worker", Direct: true})
// after
js.DeleteConsumer(stream, "worker")
js.AddConsumer(stream, &nats.ConsumerConfig{Durable: "worker-direct", Direct: true})
Defensive patterns

Strategy: validation

Validate before calling

if existing.Direct != desired.Direct {
	return errors.New("direct cannot be updated; use a new consumer")
}

Prevention

When it happens

Trigger: js.UpdateConsumer on an existing durable where the proposed config's Direct field differs from the stored one (e.g. updating a normal durable to Direct: true, or a direct consumer to Direct: false).

Common situations: Migrating an app to direct consumers for read-only replays while reusing old durable names; tooling that always sends the full config flipping the flag; partial upgrades where one service created a direct consumer and another updates it as a normal one.

Related errors


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