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
- Ensure the slot's existing task is stopped and its handle taken (via the slot's take/drop path) before calling insert again.
- 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).
- 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
- Treat each TaskSlot as single-owner: always pair spawn+insert with take/abort cleanup
- Guard client connect/initialize paths with a once flag so tasks cannot be spawned twice
- In tests, cover reconnect and re-init flows to surface double-insert bugs early
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
- Failed to start Lighter WebSocket handler task: {e}
- analysis task did not complete: {join_error}
- System command sender should be initialized by runner
- Execution event sender should be initialized by runner
- Failed to create tokio runtime
AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08).
Data as JSON: /api/errors/bc49942166d3986a.
Report an issue: GitHub.