nautechsystems/nautilus_trader · error · TransportError::Io(std::io::Error)

Heartbeat interval cannot be zero

Error message

Heartbeat interval cannot be zero

What it means

The WebSocket client validates config invariants at construction. A heartbeat interval of exactly `Some(0)` seconds is rejected with `InvalidInput` because a zero-period heartbeat timer is meaningless (it would fire every loop iteration or never schedule correctly).

Source

Thrown at crates/network/src/websocket/client.rs:228

        clippy::unused_async_trait_impl,
        reason = "async signature for consistency with connect-based constructors"
    )]
    pub async fn new_with_writer(
        config: WebSocketConfig,
        writer: MessageWriter,
    ) -> Result<Self, TransportError> {
        Self::new_with_writer_and_state_sink(config, writer, None)
    }

    fn new_with_writer_and_state_sink(
        mut config: WebSocketConfig,
        writer: MessageWriter,
        state_sink: Option<SocketStateSink>,
    ) -> Result<Self, TransportError> {
        install_cryptographic_provider();

        if config.heartbeat_interval_secs == Some(0) {
            return Err(TransportError::Io(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                "Heartbeat interval cannot be zero",
            )));
        }

        let connection_mode = Arc::new(AtomicU8::new(ConnectionMode::Reconnect.as_u8()));
        let connection_epoch = Arc::new(AtomicU64::new(0));
        let state_notify = Arc::new(tokio::sync::Notify::new());
        let controller_notify = Arc::new(tokio::sync::Notify::new());
        let reconnect_published = Arc::new(AtomicBool::new(true));
        let outcome =
            ConnectionMode::complete_reconnect_with_sink(&connection_mode, state_sink.as_ref());
        debug_assert_eq!(outcome, ReconnectOutcome::Reconnected);

        // Note: We don't spawn a read task here since the reader is handled externally
        let read_task = None;
        let read_fence = None;

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Use `None` to disable heartbeats instead of `Some(0)`.
  2. Set a positive interval such as `Some(15)` or `Some(30)` seconds.
  3. Clamp the value when loading from config: treat 0 as disabled.

Example fix

// before
let config = WebSocketConfig { heartbeat_interval_secs: Some(0), .. };
// after
let config = WebSocketConfig { heartbeat_interval_secs: Some(15), .. }; // or None to disable
Defensive patterns

Strategy: validation

Validate before calling

fn sane_heartbeat(secs: Option<u64>) -> Option<u64> {
    match secs { Some(0) | None => None, s => s } // 0 means disabled
}
// then: heartbeat_interval_secs: sane_heartbeat(raw_value)

Try / catch

match WebSocketClient::new_with_writer_and_state_sink(...) {
    Err(e) if e.to_string().contains("Heartbeat interval cannot be zero") => {
        eprintln!("config error: use None to disable heartbeat");
        return;
    }
    Err(e) => return Err(e),
    Ok(c) => c,
}

Prevention

When it happens

Trigger: Building `WebSocketConfig` (often by struct literal, bypassing the builder) with `heartbeat_interval_secs: Some(0)` and passing it to `new_with_writer_and_state_sink`.

Common situations: Users intending 'no heartbeat' setting 0 instead of `None`, or computing the interval from a config value that defaults to 0 when unset.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


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