nautechsystems/nautilus_trader · error

heartbeat_timeout_secs must cover at least two server heartb

Error message

heartbeat_timeout_secs must cover at least two server heartbeat intervals ({DEAD_PEER_TIMEOUT_MIN_SECS}s), was {timeout_secs}s

What it means

The Betfair stream config requires that an explicitly configured dead-peer timeout (heartbeat_timeout_secs) be at least DEAD_PEER_TIMEOUT_MIN_SECS seconds — two server heartbeat intervals — so the client does not declare the peer dead before a second missed heartbeat could arrive. Shorter values fail validation in StreamConfig::validate.

Source

Thrown at crates/adapters/betfair/src/stream/config.rs:85

        self.heartbeat_timeout_secs
            .unwrap_or(DEAD_PEER_TIMEOUT_MIN_SECS)
    }

    /// Validates heartbeat settings.
    ///
    /// # Errors
    ///
    /// Returns an error if an outbound interval is zero or an explicit dead-peer timeout is
    /// shorter than two server intervals.
    pub fn validate(&self) -> anyhow::Result<()> {
        if self.heartbeat_secs == Some(0) {
            anyhow::bail!("heartbeat_secs must be positive when set");
        }

        if let Some(timeout_secs) = self.heartbeat_timeout_secs
            && timeout_secs < DEAD_PEER_TIMEOUT_MIN_SECS
        {
            anyhow::bail!(
                "heartbeat_timeout_secs must cover at least two server heartbeat intervals \
                 ({DEAD_PEER_TIMEOUT_MIN_SECS}s), was {timeout_secs}s",
            );
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use rstest::rstest;

    use super::*;

    #[rstest]
    fn test_stream_config_defaults() {
        let config = BetfairStreamConfig::default();

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Raise heartbeat_timeout_secs to at least DEAD_PEER_TIMEOUT_MIN_SECS (two server heartbeat intervals).
  2. Leave heartbeat_timeout_secs as None to use the adapter's default dead-peer policy.
  3. Reduce heartbeat_secs first if you need faster detection, then keep the timeout at >= 2x that interval.

Example fix

// before
config.heartbeat_secs = Some(5);
config.heartbeat_timeout_secs = Some(3); // below minimum, fails
// after
config.heartbeat_secs = Some(5);
config.heartbeat_timeout_secs = Some(10); // covers two intervals
Defensive patterns

Strategy: validation

Validate before calling

if let Some(t) = heartbeat_timeout_secs {
    let min = 2 * heartbeat_secs.unwrap_or_default();
    assert!(t >= min && t >= DEAD_PEER_TIMEOUT_MIN_SECS, "timeout too short");
}

Try / catch

match config.validate() {
    Err(e) if e.to_string().contains("heartbeat_timeout_secs") =>
        eprintln!("raise heartbeat_timeout_secs to >= 2x heartbeat interval"),
    Err(e) => eprintln!("config invalid: {e}"),
    Ok(()) => {},
}

Prevention

When it happens

Trigger: Setting heartbeat_timeout_secs to a value smaller than DEAD_PEER_TIMEOUT_MIN_SECS (e.g. 5s when the minimum is 10s for a 5s heartbeat) and calling connect via connect_inner -> validate.

Common situations: Users trying to 'fail fast' on dead connections by lowering the timeout below the safe minimum; copying a timeout tuned for another exchange's faster heartbeats; guessing the constraint because it is only stated in the validate doc comment.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/5d0529f37ad48815. Report an issue: GitHub.