nats-io/nats-server · error · JSStreamInvalidConfigError

counter stream cannot use discard new

Error message

counter stream cannot use discard new

What it means

A counter stream (AllowMsgCounter=true) tracks per-message counter values and relies on messages never being proactively evicted on publish, so the DiscardNew policy (discard oldest when limits reached) is incompatible. server/stream.go validation rejects Discard == DiscardNew for counter streams. Returned as a JSStreamInvalidConfigError.

Source

Thrown at server/stream.go:1996

	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"))
		}
	}

	// Check for new discard new per subject, we require the discard policy to also be new.
	if cfg.DiscardNewPer {
		if cfg.Discard != DiscardNew {
			return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("discard new per subject requires discard new policy to be set"))
		}
		if cfg.MaxMsgsPer <= 0 {

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Set StreamConfig.Discard to DiscardOld when AllowMsgCounter is true.
  2. Or disable AllowMsgCounter if DiscardNew semantics are actually required.
  3. Rely on MaxMsgs/limits only with DiscardOld, or use limits retention with no discard pressure.
  4. Review newer NATS server versions' counter documentation for compatible discard policies.

Example fix

// before
 StreamConfig{Name: "CTR", AllowMsgCounter: true, Discard: jetstream.DiscardNew}
// after
 StreamConfig{Name: "CTR", AllowMsgCounter: true, Discard: jetstream.DiscardOld}
Defensive patterns

Strategy: validation

Validate before calling

if cfg.AllowMsgCounter && cfg.Discard == jetstream.DiscardNew {
    return fmt.Errorf("counter streams require Discard=DiscardOld")
}

Try / catch

var scErr *jetstream.JSApiError
if _, err := js.CreateStream(ctx, cfg); errors.As(err, &scErr) && strings.Contains(scErr.Description, "counter stream cannot use discard new") {
    cfg.Discard = jetstream.DiscardOld
    _, err = js.CreateStream(ctx, cfg)
}

Prevention

When it happens

Trigger: AddStream/UpdateStream with AllowMsgCounter=true and Discard=DiscardNew (or DiscardPerSubject defaulting to DiscardNew semantics for counters).

Common situations: Building a counter/atomic-increment stream but leaving the common default DiscardNew in place; copying a normal stream config and adding AllowMsgCounter without removing DiscardNew.

Related errors


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