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
- Delete the consumer and recreate it with the desired Direct setting.
- Use a distinct durable/name for the direct variant.
- Match the existing consumer's Direct value in update requests.
- 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
- Use separate durable names for direct vs normal consumers
- Read ConsumerInfo before constructing update requests
- Never send full config blindly; diff against current state
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
- deliver policy can not be updated
- storage type can not be updated
- sourcing can not be updated
- start sequence can not be updated
- start time can not be updated
AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02).
Data as JSON: /api/errors/d5baf7dc38ee88d7.
Report an issue: GitHub.