nautechsystems/nautilus_trader · error · Error::Io(std::io::Error)
Timed out joining WebSocket handler task after abort: {error
Error message
Timed out joining WebSocket handler task after abort: {error} What it means
When closing a WebSocket stream (during connect rollback, close_locked, or a timeout test path), the OKX client aborts its handler task and waits `timeout` for it to finish via finish_shutdown. If the task does not join within the timeout, an io::Error of kind TimedOut is raised: the handler task is stuck and shutdown is incomplete, risking leaked tasks or sockets.
Source
Thrown at crates/adapters/okx/src/websocket/client.rs:1060
// Wipe per-base-pair refcounts so a subsequent reconnect can re-arm
// the index-tickers channel. Otherwise the stale count short-circuits
// every future `subscribe_index_prices` call and the feed stays dark.
self.index_pair_subscribers.clear();
if let Some(control) = &self.socket_control {
control.deregister();
}
log::debug!("Close process completed");
task_result
}
async fn close_stream_task(&self, timeout: Duration) -> Result<(), Error> {
match self.handler_tasks.finish_shutdown(timeout, timeout).await {
Ok(()) => Ok(()),
Err(error @ TaskShutdownError::Timeout { .. }) => Err(Error::Io(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("Timed out joining WebSocket handler task after abort: {error}"),
))),
Err(e) => Err(Error::Io(std::io::Error::other(format!(
"WebSocket handler shutdown failed: {e}"
)))),
}
}
/// Get active subscriptions for a specific instrument.
pub fn get_subscriptions(&self, instrument_id: InstrumentId) -> Vec<OKXWsChannel> {
let symbol = instrument_id.symbol.inner();
let mut channels = Vec::new();
for entry in self.subscriptions_inst_id.iter() {
let (channel, instruments) = entry.pair();
if instruments.contains(&symbol) {
channels.push(channel.clone());View on GitHub (pinned to 18893faf8b)
Solutions
- Increase the shutdown timeout if it is shorter than the handler's worst-case wake-up latency.
- Ensure all consumers of the handler task's channels keep draining so it can observe the abort and exit.
- Check for lock contention or blocking calls inside the handler loop that prevent reacting to cancellation.
- Enable TCP keepalive / read timeouts on the socket so a dead connection actually errors out and unblocks the reader.
- If it occurs during connect failure, inspect the original connect error; this timeout is a secondary symptom.
Example fix
// before client.close_stream_task(Duration::from_millis(50)).await?; // after client.close_stream_task(Duration::from_secs(5)).await?;
Defensive patterns
Strategy: try-catch
Try / catch
match client.close_stream_task(Duration::from_secs(5)).await {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::TimedOut => {
log::error!("handler task did not join: {e}; check channel consumers/locks");
}
Err(e) => return Err(e.into()),
} Prevention
- Always drain channels the handler task writes to so it can exit on abort
- Avoid blocking calls or long-held locks inside WS handler loops
- Set socket read/keepalive timeouts so stalled connections wake the reader
- Size the shutdown timeout to exceed worst-case handler wake latency
When it happens
Trigger: Calling connect (which rolls back via close_stream_task on failure), close_locked during disconnect, or otherwise aborting a WebSocket handler task whose read loop or message pump is blocked (e.g. blocked on a synchronous send into a full channel, or a lock held elsewhere).
Common situations: Underlying TCP connection stalled without triggering read timeout so the reader task never wakes; a consumer of the handler's output channel stopped reading (backpressure); deadlock with another lock; overly short shutdown timeout configured.
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.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Failed to terminate {owner} tasks: {e}
- Hyperliquid WebSocket handler did not stop after abort
- Lighter WebSocket handler did not stop after abort
- Timeout waiting for account {account_id} to be registered af
- Architect AX data WebSocket handler did not stop after abort
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/4e65f86737145261.
Report an issue: GitHub.