nats-io/nats-server · error · JSStreamInvalidConfigError
roll-ups require the purge permission
Error message
roll-ups require the purge permission
What it means
AllowRollup lets publishers replace or truncate stream contents via rollup headers, which destroys data, so the config forbids combining it with DenyPurge (which explicitly revokes destructive purge operations). Validation in server/stream.go returns a JSStreamInvalidConfigError when both flags are set.
Source
Thrown at server/stream.go:1990
}
}
if cfg.Duplicates < 0 {
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("duplicates window can not be negative"))
}
// Check that duplicates is not larger then age if set.
if cfg.MaxAge != 0 && cfg.Duplicates > cfg.MaxAge {
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("duplicates window can not be larger then max age"))
}
if lim.Duplicates > 0 && cfg.Duplicates > lim.Duplicates {
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("duplicates window can not be larger then server limit of %v",
lim.Duplicates.String()))
}
if cfg.Duplicates > 0 && cfg.Duplicates < 100*time.Millisecond {
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("duplicates window needs to be >= 100ms"))
}
if cfg.DenyPurge && cfg.AllowRollup {
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("roll-ups require the purge permission"))
}
// Counter is not compatible with some settings.
if cfg.AllowMsgCounter {
if cfg.Discard == DiscardNew {
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("counter stream cannot use discard new"))
}
if cfg.AllowMsgTTL {
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("counter stream cannot use message TTLs"))
}
if cfg.AllowMsgSchedules {
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("counter stream cannot use message schedules"))
}
if cfg.Retention != LimitsPolicy {
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("counter stream can only use limits retention"))
}
}
View on GitHub (pinned to 3a66a489d2)
Solutions
- Set DenyPurge=false if rollups are required for the stream.
- Or set AllowRollup=false if purge must remain denied.
- Split into two streams: one with rollups enabled, one restricted without.
- Control destructive access at the user/permission level instead of mixing these stream flags.
Example fix
// before
StreamConfig{Name: "SNAP", AllowRollup: true, DenyPurge: true}
// after
StreamConfig{Name: "SNAP", AllowRollup: true, DenyPurge: false} Defensive patterns
Strategy: validation
Validate before calling
if cfg.AllowRollup && cfg.DenyPurge {
return fmt.Errorf("AllowRollup conflicts with DenyPurge; pick one")
} Try / catch
var scErr *jetstream.JSApiError
if _, err := js.UpdateStream(ctx, cfg); errors.As(err, &scErr) && strings.Contains(scErr.Description, "roll-ups require the purge permission") {
cfg.DenyPurge = false
_, err = js.UpdateStream(ctx, cfg)
} Prevention
- When enabling rollups, explicitly set DenyPurge=false and note it in code review.
- Copy permission flags between streams only after checking both flags.
- Manage destructive-operation access at user permissions, not via these flags together.
When it happens
Trigger: AddStream/UpdateStream with StreamConfig.AllowRollup=true and DenyPurge=true, typically when configuring permissions around rollups per subject.
Common situations: Operators hardening streams by denying purge while separately enabling rollups for snapshot-style publishes; copying permission flags from another stream without realizing the interaction.
Related errors
- JS_STREAM_ROLLUP_FAILED
- JS_STREAM_MSG_DELETE_FAILED
- JS_STREAM_PURGE_FAILED
- duplicates window can not be negative
- duplicates window can not be larger then max age
AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02).
Data as JSON: /api/errors/73c3d8ce84f49643.
Report an issue: GitHub.