nats-io/nats-server · error · JSStreamInvalidConfigError

flow control ack policy heartbeat needs to be 1s

Error message

flow control ack policy heartbeat needs to be 1s

What it means

When a consumer's AckPolicy is flow-control-based (acked flow control, paired with source/sourcing consumers), the server requires config.Heartbeat to be exactly sourceHealthHB (1s), because heartbeats are also used to detect stalled ephemeral sourcing consumers. Any other heartbeat interval is rejected with this JSStreamInvalidConfigError at validation time.

Source

Thrown at server/consumer.go:793

		}
	}
	if config.AckWait < 0 {
		return NewJSConsumerAckWaitNegativeError()
	}

	// Ack Flow Control policy requires push-based flow-controlled consumer.
	if config.AckPolicy == AckFlowControl {
		if config.DeliverSubject == _EMPTY_ {
			return NewJSConsumerAckFCRequiresPushError()
		}
		if !config.FlowControl {
			return NewJSConsumerAckFCRequiresFCError()
		}
		// We currently limit using heartbeat of 1s, since those are used for ephemeral sourcing consumers as well.
		// We could decide to relax this in the future, but need to be careful to not allow a heartbeat larger
		// than the stalled source timeout.
		if config.Heartbeat != sourceHealthHB {
			return NewJSStreamInvalidConfigError(fmt.Errorf("flow control ack policy heartbeat needs to be 1s"))
		}
		if config.MaxAckPending <= 0 {
			return NewJSConsumerAckFCRequiresMaxAckPendingError()
		}
		if config.AckWait != 0 || len(config.BackOff) > 0 {
			return NewJSConsumerAckFCRequiresNoAckWaitError()
		}
		if config.MaxDeliver > 0 {
			return NewJSConsumerAckFCRequiresNoMaxDeliverError()
		}
	}

	// Check if we have a BackOff defined that MaxDeliver is within range etc.
	if lbo := len(config.BackOff); lbo > 0 && config.MaxDeliver != -1 && lbo > config.MaxDeliver {
		return NewJSConsumerMaxDeliverBackoffError()
	}

	if len(config.Description) > JSMaxDescriptionLen {

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Set Heartbeat (client-side IdleHeartbeat) to exactly 1s (time.Second / "1s" in JSON config).
  2. If a different heartbeat is required, switch AckPolicy off the flow-control policy (e.g. explicit/all) so this constraint no longer applies.
  3. Also satisfy the companion constraints checked nearby: MaxAckPending > 0 and AckWait == 0 with empty BackOff.

Example fix

// before
cc := &nats.ConsumerConfig{AckPolicy: nats.AckFCPolicy, Heartbeat: 2 * time.Second}
// after
cc := &nats.ConsumerConfig{AckPolicy: nats.AckFCPolicy, Heartbeat: time.Second}
Defensive patterns

Strategy: validation

Validate before calling

if cc.AckPolicy == nats.AckFCPolicy && cc.Heartbeat != time.Second {
    return fmt.Errorf("flow control ack policy requires Heartbeat == 1s, got %v", cc.Heartbeat)
}
if cc.AckPolicy == nats.AckFCPolicy && cc.MaxAckPending <= 0 {
    return fmt.Errorf("flow control ack policy requires MaxAckPending > 0")
}

Type guard

func fcAckConfigValid(hb time.Duration, maxAckPending int) bool {
    return hb == time.Second && maxAckPending > 0
}

Try / catch

_, err := js.AddConsumer(stream, cc)
var cerr *nats.APIError
if errors.As(err, &cerr) && strings.Contains(cerr.Description, "heartbeat needs to be 1s") {
    cc.Heartbeat = time.Second
    return js.AddConsumer(stream, cc)
}

Prevention

When it happens

Trigger: Creating/updating a consumer with AckPolicy set to the flow-control ack policy and Heartbeat set to anything other than 1s (e.g. 2s, 500ms, or via the client library's Heartbeat/IdleHeartbeat field).

Common situations: Tuning heartbeats for slower networks and picking 2-5s while also enabling flow control acks; copying sourcing consumer examples that omit Heartbeat (zero is also != 1s).

Related errors


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