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
- Read the wrapped inner error from wait_until_active() to see if it was a close, auth, or timeout.
- Verify the WebSocket URL points to the correct Lighter environment (mainnet/testnet).
- Retry connect after a delay; transient readiness failures are common during venue maintenance.
- 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
- Confirm the Lighter WS URL matches the intended network (mainnet/testnet)
- Add bounded reconnect with exponential backoff
- Check for proxies/firewalls that kill idle WebSockets
- Avoid cancelling the cancellation token during connect
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
- {e}; failed to roll back partial Lighter connection: {shutdo
- subscription state lock poisoned
- InstrumentState channel requires kind and currency parameter
- std::mem::take(&mut self.shutdown_errors).join("; ")
- noid '{}' does not match new order oid '{}'
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/b49e77b98d7345a1.
Report an issue: GitHub.