nautechsystems/nautilus_trader · error

Lighter WebSocket did not reach active state

Error message

Lighter WebSocket did not reach active state

What it means

LighterMarketDataClient::spawn_ws connects the venue WebSocket and then waits for it to reach 'active' state via wait_until_active(). If the wait errors (handshake accepted but the socket never becomes active, subscription/auth rejection, or connection dropped), the client rolls back (disconnects, finishes the retained handler) and returns the readiness error, optionally wrapped with rollback failure details.

Source

Thrown at crates/adapters/lighter/src/data/mod.rs:419

            .context("failed to connect to Lighter WebSocket")?;

        if let Err(e) = ws_guard.client_mut().wait_until_active().await {
            let ws_client = ws_guard.disarm();
            let mut rollback_errors = Vec::new();

            if let Err(e) = ws_client
                .disconnect_with_task_retention(Arc::clone(&self.ws_handler_retained))
                .await
            {
                rollback_errors.push(e.to_string());
            }

            if let Err(e) = self.ws_handler_retained.finish().await {
                rollback_errors.push(e.to_string());
            }

            let readiness_error =
                anyhow::Error::new(e).context("Lighter WebSocket did not reach active state");

            if rollback_errors.is_empty() {
                return Err(readiness_error);
            }
            return Err(readiness_error.context(format!(
                "Lighter WebSocket readiness rollback failed: {}",
                rollback_errors.join("; ")
            )));
        }

        let mut ws_client = ws_guard.disarm();
        self.ws_client.set_task_slot(ws_client.take_task_slot());

        let cancellation_token = self.cancellation_token.clone();
        let data_sender = self.data_sender.clone();
        let market_stats_subscriptions = Arc::clone(&self.market_stats_subscriptions);

        let future = async move {

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Read the wrapped inner error from wait_until_active() to see if it was a close, auth, or timeout.
  2. Verify the WebSocket URL points to the correct Lighter environment (mainnet/testnet).
  3. Retry connect after a delay; transient readiness failures are common during venue maintenance.
  4. Check network/proxy settings that may terminate WebSocket upgrades; confirm cancellation token is not being cancelled early.

Example fix

// before
client.connect().await?; // assumes WS will go active
// after
match client.connect().await { Err(e) if is_readiness(&e) => { tokio::time::sleep(RETRY_DELAY).await; client.connect().await } r => r }
Defensive patterns

Strategy: retry

Validate before calling

// Rust: check config before connect
assert!(!ws_url.is_empty());
assert!(ws_url.starts_with("wss://"));

Try / catch

match client.connect().await {
    Err(e) if format!("{e:#}").contains("did not reach active state") => {
        tokio::time::sleep(Duration::from_secs(2)).await;
        client.connect().await?; // bounded retries
    }
    r => r?,
}

Prevention

When it happens

Trigger: connect_with_cancellation succeeds but wait_until_active() returns Err — e.g. WS closed before active, auth failure on connect, or server never sends the active/ready signal; possibly also if cancellation token is cancelled mid-wait.

Common situations: Lighter WS endpoint outage or maintenance, wrong network (mainnet vs testnet) URL, firewall/proxy dropping long-lived WebSockets, or an API key lacking data permissions.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


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