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.

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.

Appendix: 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)