nautechsystems/nautilus_trader · critical

task slot is already occupied

Error message

task slot is already occupied

What it means

This panic comes from `TaskSlot::insert` in the live execution engine's task runner. A task slot holds at most one `JoinHandle`; calling `insert` while the slot already owns a task aborts the NEW handle and panics, because two tasks sharing one slot would orphan a join handle and leak the running task. It is an internal lifecycle invariant of the live engine, not a condition user data can normally cause.

Source

Thrown at crates/live/src/task.rs:648

    pub const fn is_none(&self) -> bool {
        self.handle.is_none()
    }

    /// Returns the owned task handle, when present.
    #[must_use]
    pub const fn as_ref(&self) -> Option<&JoinHandle<T>> {
        self.handle.as_ref()
    }

    /// Stores a task in an empty slot.
    ///
    /// # Panics
    ///
    /// Aborts `handle` and panics if the slot already owns a task.
    pub fn insert(&mut self, handle: JoinHandle<T>) {
        if self.handle.is_some() {
            handle.abort();
            panic!("task slot is already occupied");
        }
        self.handle = Some(handle);
        self.abort_requested = false;
    }

    /// Requests task cancellation and records it as owner-initiated.
    pub fn abort(&mut self) {
        if let Some(handle) = self.handle.as_ref() {
            handle.abort();
            self.abort_requested = true;
        }
    }

    fn complete(&mut self, result: Result<T, JoinError>) -> TaskJoinOutcome<T> {
        let outcome = match result {
            Ok(output) => TaskJoinOutcome::Completed(output),
            Err(e) if e.is_cancelled() && self.abort_requested => TaskJoinOutcome::Aborted,
            Err(e) => TaskJoinOutcome::Failed(e),

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Ensure the slot's existing task is stopped and its handle taken (via the slot's take/drop path) before calling insert again.
  2. Fix the double-spawn: guard the client's spawn path so the task is started only once per slot (e.g. an `is_running`/once flag).
  3. If replacing a task is intended, abort/detach the old handle explicitly with a different API rather than re-inserting into the occupied slot.

Example fix

// before
slot.insert(spawn_heartbeat()); // panics on reconnect because old handle still in slot

// after
if let Some(old) = slot.take() {
    old.abort();
}
slot.insert(spawn_heartbeat());
Defensive patterns

Strategy: validation

Validate before calling

// Rust: check the slot is free before inserting
assert!(slot.is_empty(), "task slot still owns a running task; take() it first");
slot.insert(handle);

Prevention

When it happens

Trigger: Calling `slot.insert(handle)` twice without an intervening `take()`/completion of the slot's current task; e.g. a live data or exec client spawning its heartbeat/reconnect/task twice for the same slot (duplicate connect(), double initialization, or re-registering a client task after a reconnect while the old handle was never taken).

Common situations: Live adapter bugs where `connect()` is called twice; a client re-initialization path that re-spawns a heartbeat task; tests or custom adapters reusing one TaskSlot for sequential tasks without calling the take/cleanup method in between.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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