nats-io/nats-server · error · JSStreamInvalidConfigError

max age can not be negative

Error message

max age can not be negative

What it means

JetStream rejects a stream config whose MaxAge (max message retention duration) is negative. MaxAge must be zero (use stream default) or a positive duration; this is a hard JSStreamInvalidConfigError regardless of pedantic mode.

Source

Thrown at server/stream.go:1951

		if pedantic && cfg.MaxBytes < -1 {
			return StreamConfig{}, NewJSPedanticError(fmt.Errorf("max_bytes must be set to -1"))
		}
		cfg.MaxBytes = -1
	}
	if cfg.MaxMsgSize == 0 || cfg.MaxMsgSize < -1 {
		if pedantic && cfg.MaxMsgSize < -1 {
			return StreamConfig{}, NewJSPedanticError(fmt.Errorf("max_msg_size must be set to -1"))
		}
		cfg.MaxMsgSize = -1
	}
	if cfg.MaxConsumers == 0 || cfg.MaxConsumers < -1 {
		if pedantic && cfg.MaxConsumers < -1 {
			return StreamConfig{}, NewJSPedanticError(fmt.Errorf("max_consumers must be set to -1"))
		}
		cfg.MaxConsumers = -1
	}
	if cfg.MaxAge < 0 {
		return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("max age can not be negative"))
	}
	if cfg.MaxAge != 0 && cfg.MaxAge < 100*time.Millisecond {
		return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("max age needs to be >= 100ms"))
	}

	if cfg.Duplicates == 0 && cfg.Mirror == nil && len(cfg.Sources) == 0 {
		maxWindow := StreamDefaultDuplicatesWindow
		if lim.Duplicates > 0 && maxWindow > lim.Duplicates {
			if pedantic {
				return StreamConfig{}, NewJSPedanticError(fmt.Errorf("duplicate window limits are higher than current limits"))
			}
			maxWindow = lim.Duplicates
		}
		if cfg.MaxAge != 0 && cfg.MaxAge < maxWindow {
			if pedantic {
				return StreamConfig{}, NewJSPedanticError(fmt.Errorf("duplicate window cannot be bigger than max age"))
			}
			cfg.Duplicates = cfg.MaxAge

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Fix the duration computation so MaxAge is non-negative (e.g. 30*time.Minute)
  2. Set MaxAge to 0 to use the server default retention window
  3. Clamp with a guard: if maxAge < 0 { maxAge = 0 } before submitting

Example fix

// before
MaxAge: time.Until(deadline) // deadline already passed -> negative
// after
if d := time.Until(deadline); d > 0 { cfg.MaxAge = d } else { cfg.MaxAge = 0 }
Defensive patterns

Strategy: validation

Validate before calling

if cfg.MaxAge < 0 {
    return fmt.Errorf("MaxAge cannot be negative; got %v", cfg.MaxAge)
}

Try / catch

if err != nil {
    var apiErr *nats.APIError
    if errors.As(err, &apiErr) && apiErr.ErrorCode == nats.ErrorCodeJetStreamInvalidStreamConfig {
        // log the raw MaxAge value for diagnosis, then fix or clamp
        cfg.MaxAge = 0
        err = createStream(cfg)
    }
}

Prevention

When it happens

Trigger: Stream create/update with cfg.MaxAge < 0, e.g. time.Duration computed as -30*time.Minute or an unparsed/wrong-unit value yielding a negative duration. server/stream.go:1951.

Common situations: Retention computed from a deadline in the past (now - expiry); config parsing where a string like '-1h' was passed; arithmetic on time.Duration in the wrong unit producing overflow/negative values.

Related errors


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