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

  1. Set DenyPurge=false if rollups are required for the stream.
  2. Or set AllowRollup=false if purge must remain denied.
  3. Split into two streams: one with rollups enabled, one restricted without.
  4. 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 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


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