nautechsystems/nautilus_trader · error · anyhow::Error

Failed to start Hyperliquid WebSocket handler task: {e}

Error message

Failed to start Hyperliquid WebSocket handler task: {e}

What it means

Raised in connect_locked when tokio::spawn of the WebSocket handler task fails. The spawn failure means the tokio runtime refused to start the task, most commonly because the runtime is shutting down, so no handler exists to process commands or read the WebSocket and the connection attempt is aborted. The client resets out_rx and releases reserved rate-limiter slots before returning.

Source

Thrown at crates/adapters/hyperliquid/src/websocket/client.rs:507

                        }
                    }
                    None => {
                        if handler.is_stopped() {
                            log::debug!("Stop signal received, ending message processing");
                            break;
                        }
                        log::warn!("WebSocket stream ended unexpectedly");
                        break;
                    }
                }
            }
            rate_limits.release_client(client_id);
            connection_permit.lock().take();
            log::debug!("Handler task completed");
        }) {
            self.out_rx = None;
            self.release_limit_reservations();
            anyhow::bail!("Failed to start Hyperliquid WebSocket handler task: {e}");
        }
        Ok(())
    }

    pub fn set_post_timeout(&mut self, timeout: Duration) {
        self.post_timeout = timeout;
    }

    pub(crate) fn begin_shutdown(&self) {
        self.signal.store(true, Ordering::Relaxed);
    }

    /// Replaces state owned by a terminated WebSocket generation.
    ///
    /// This must run only after the handler task has stopped. Replacing the
    /// shared containers, rather than clearing them, prevents old clones or
    /// in-flight work from mutating a subsequent connection generation.
    pub(crate) fn reset_runtime_state(&mut self) {

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Ensure connect() is only awaited while the tokio runtime is alive; move reconnection logic into a normally spawned task.
  2. Check whether the process/runtime is shutting down and stop reconnect attempts when a shutdown signal is observed.
  3. Keep the Runtime/Handle alive for the client's whole lifetime and shut it down only after dropping clients.
  4. Retry connect() on a live runtime if the failure was transient shutdown overlap.

Example fix

// before: connect from Drop while runtime shuts down / impl Drop for MyService { fn drop(&mut self) { rt.block_on(client.connect()); } } / // after: connect inside a live runtime task / async fn run(service: MyService) -> anyhow::Result<()> { service.client.connect().await?; service.run().await }
Defensive patterns

Strategy: retry

Validate before calling

fn can_connect(shutdown_requested: bool) -> bool { !shutdown_requested }

Try / catch

match client.connect().await { Err(e) if e.to_string().contains("handler task") && shutdown_requested() => { log::info!("skipping reconnect: runtime shutting down"); } Err(e) => return Err(e.into()), Ok(()) => {} }

Prevention

When it happens

Trigger: Calling connect() (via connect_locked) while the enclosing tokio runtime is shutting down or has already been dropped, so tokio::spawn cannot register the new handler task.

Common situations: Calling connect() from a Drop impl or shutdown hook; a reconnect loop still running while the runtime tears down; dropping the Runtime/Handle while the client lives; blocking a runtime until shutdown while retries continue.

Related errors


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