nautechsystems/nautilus_trader · error · anyhow::Error
Hyperliquid allows at most {} WebSocket connections per rout
Error message
Hyperliquid allows at most {} WebSocket connections per route What it means
Raised in WebSocketClient::connect_locked when acquiring a semaphore permit from connection_slots fails via try_acquire_owned, meaning all slots for the route are held. Hyperliquid enforces a cap of HYPERLIQUID_WS_CONNECTIONS_MAX simultaneous WebSocket connections per route to respect venue rate limits. The error reports the configured maximum so operators can size their connection usage.
Source
Thrown at crates/adapters/hyperliquid/src/websocket/client.rs:284
let _guard = connect_lock.lock().await;
self.connect_locked().await
}
async fn connect_locked(&mut self) -> anyhow::Result<()> {
if self.is_active() {
log::warn!("WebSocket already connected");
return Ok(());
}
if !self.task_handle.is_empty() {
self.disconnect_locked().await?;
}
if self.connection_permit.lock().is_none() {
let permit = Arc::clone(&self.rate_limits.connection_slots)
.try_acquire_owned()
.map_err(|_| {
anyhow::anyhow!(
"Hyperliquid allows at most {} WebSocket connections per route",
crate::common::consts::HYPERLIQUID_WS_CONNECTIONS_MAX,
)
})?;
*self.connection_permit.lock() = Some(permit);
}
// A fresh socket has no venue-side subscriptions; stale book stream
// entries must not gate the venue subscribe for re-subscriptions
self.book_streams.clear();
let (message_handler, raw_rx) = channel_message_handler();
let cfg = WebSocketConfig {
url: self.url.clone(),
headers: vec![],
heartbeat_interval_secs: None,
heartbeat_payload: None,
connect_timeout_ms: Some(15_000),View on GitHub (pinned to 18893faf8b)
Solutions
- Share a single WebSocket client and multiplex subscriptions instead of creating one client per instrument
- Ensure unused clients are closed/dropped so their permits are released; look for leaked clients holding the semaphore
- Raise HYPERLIQUID_WS_CONNECTIONS_MAX (crate::common::consts) if the venue allows more connections for your use case
- Retry the connect with backoff after other connections close (the permit is a try_acquire, not a wait)
Example fix
// before
for instrument in instruments {
let client = WebSocketClient::new(...); // one client per instrument -> exhausts permits
client.connect().await?;
}
// after
let client = WebSocketClient::new(...); // single shared client
client.connect().await?;
for instrument in instruments {
client.subscribe_book(...).await?; // multiplex subscriptions on one connection
} Defensive patterns
Strategy: retry
Validate before calling
# Python/Rust: count active clients before opening another connection assert active_ws_clients() < HYPERLIQUID_WS_CONNECTIONS_MAX, "too many WebSocket connections"
Try / catch
// Rust
loop {
match client.connect().await {
Ok(()) => break,
Err(e) if e.to_string().contains("WebSocket connections per route") => {
tokio::time::sleep(Duration::from_secs(2)).await; // backoff, retry
}
Err(e) => return Err(e),
}
} Prevention
- Share one WebSocket client across many instrument subscriptions
- Always close/drop clients you no longer use so semaphore permits are released
- Audit for leaked clients after reconfiguration or reconnect logic
- Keep concurrent processes/tests within the per-route connection cap
When it happens
Trigger: Opening a new WebSocket connection when HYPERLIQUID_WS_CONNECTIONS_MAX permits are already held — e.g. many instruments/subscriptions each using separate clients, leaked clients that connected but were never dropped/closed, or concurrent connects across the same route exceeding the cap.
Common situations: Spawning many data clients (or one per instrument) instead of sharing one client with multiple subscriptions; failing to close/drop old clients after reconfiguration so permits leak; parallel test suites or multiple processes hitting the same limit cumulatively.
Related errors
- Failed to send subscribe command: {e}
- Not a subscription channel: {kind}
- expected 2 impact prices, received {len}
- Failed to send SetDepth10Sub command: {e}
- Failed to send UpdateTradeSubs command: {e}
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/80086c7fd33443e9.
Report an issue: GitHub.