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

  1. Increase the shutdown timeout if it is shorter than the handler's worst-case wake-up latency.
  2. Ensure all consumers of the handler task's channels keep draining so it can observe the abort and exit.
  3. Check for lock contention or blocking calls inside the handler loop that prevent reacting to cancellation.
  4. Enable TCP keepalive / read timeouts on the socket so a dead connection actually errors out and unblocks the reader.
  5. 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

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.

Related errors


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