bevyengine/bevy · critical

Failed to spawn thread.

Error message

Failed to spawn thread.

What it means

TaskPool::new_internal (bevy_tasks task_pool.rs:176-222) spawns one OS thread per worker (num_threads, defaulting to available_parallelism) via thread::Builder::spawn and expects success. The panic means the OS or runtime refused thread creation: thread-count limits (ulimit -u, cgroup pids.max), insufficient memory for thread stacks, or sandbox restrictions on clone.

Source

Thrown at crates/bevy_tasks/src/task_pool.rs:220

                            let _destructor = CallOnDrop(on_thread_destroy);
                            loop {
                                let res = std::panic::catch_unwind(|| {
                                    let tick_forever = async move {
                                        loop {
                                            local_executor.tick().await;
                                        }
                                    };
                                    block_on(ex.run(tick_forever.or(shutdown_rx.recv())))
                                });
                                if let Ok(value) = res {
                                    // Use unwrap_err because we expect a Closed error
                                    value.unwrap_err();
                                    break;
                                }
                            }
                        });
                    })
                    .expect("Failed to spawn thread.")
            })
            .collect();

        Self {
            executor,
            threads,
            shutdown_tx,
        }
    }

    /// Return the number of threads owned by the task pool
    pub fn thread_num(&self) -> usize {
        self.threads.len()
    }

    /// Allows spawning non-`'static` futures on the thread pool. The function takes a callback,
    /// passing a scope object into it. The scope object provided to the callback can be used
    /// to spawn tasks. This function will await the completion of all tasks before returning.

View on GitHub (pinned to 396ca72708)

Solutions

  1. Raise the OS limit: `ulimit -u` / container pids.max / thread quota
  2. Lower requested threads: TaskPoolBuilder::num_threads, or TaskPoolOptions min_total_threads in the app
  3. Reduce custom stack_size if set very large
  4. Free memory — thread stack allocation failure also triggers this

Example fix

// before — forces 16 worker threads inside a thread-capped container
let pool = TaskPoolBuilder::new().num_threads(16).build();

// after — stay within the container's thread budget
let pool = TaskPoolBuilder::new().num_threads(2).build();
Defensive patterns

Strategy: fallback

Validate before calling

// probe the environment before building a pool in constrained runtimes
let max_threads = std::thread::available_parallelism()
    .map(|n| n.get())
    .unwrap_or(1);
let threads = max_threads.min(2); // stay under container thread caps

Prevention

When it happens

Trigger: Creating a TaskPool (Bevy's default TaskPoolPlugin does this at startup with min_total_threads/max_total_threads from TaskPoolOptions) inside a container, CI job, or sandbox with a low process/thread cap; requesting a large num_threads; a very large stack_size exhausting address space.

Common situations: Docker/Kubernetes containers with pids limits; CI sandboxes and gVisor/seccomp profiles; embedded or memory-constrained targets; over-tuned TaskPoolOptions in constrained daemons.

Related errors


AI-assisted analysis of bevyengine/bevy@396ca72708 (2026-08-20). Data as JSON: /api/errors/071de5468d83af6a. Report an issue: GitHub.