tokio-rs/tokio · error

There must be more than one worker

Error message

There must be more than one worker

What it means

spawn_pinned::find_and_incr_least_burdened_worker calls .expect('There must be more than one worker') on the iterator's min_by_key. It panics if self.workers is empty — i.e. spawn_pinned was used on a LocalPoolSet that has zero workers.

Source

Thrown at tokio-util/src/task/spawn_pinned.rs:320

                }
            }
        })
    }

    /// Find the worker with the least number of tasks, increment its task
    /// count, and return its handle. Make sure to actually spawn a task on
    /// the worker so the task count is kept consistent with load.
    ///
    /// A job count guard is also returned to ensure the task count gets
    /// decremented when the job is done.
    fn find_and_incr_least_burdened_worker(&self) -> (&LocalWorkerHandle, JobCountGuard) {
        loop {
            let (worker, task_count) = self
                .workers
                .iter()
                .map(|worker| (worker, worker.task_count.load(Ordering::SeqCst)))
                .min_by_key(|&(_, count)| count)
                .expect("There must be more than one worker");

            // Make sure the task count hasn't changed since when we choose this
            // worker. Otherwise, restart the search.
            if worker
                .task_count
                .compare_exchange(
                    task_count,
                    task_count + 1,
                    Ordering::SeqCst,
                    Ordering::Relaxed,
                )
                .is_ok()
            {
                return (worker, JobCountGuard(Arc::clone(&worker.task_count)));
            }
        }
    }

View on GitHub (pinned to 625954f365)

Solutions

  1. Construct the LocalPoolSet with at least one worker thread.
  2. Guard against spawning after the pool has been shut down/emptied.
  3. Add an assertion at pool construction time rejecting worker_count == 0.
  4. Keep the pool alive for the duration of all spawn_pinned calls.

Example fix

// before
let pool = LocalPoolHandle::new(0); // zero workers
pool.spawn_pinned(|| async {}); // panics
// after
let pool = LocalPoolHandle::new(2);
pool.spawn_pinned(|| async {});
Defensive patterns

Strategy: validation

Validate before calling

assert!(worker_count >= 1, "LocalPoolSet needs >= 1 worker");
let pool = tokio_util::task::LocalPoolHandle::new(worker_count);

Type guard

fn valid_pool_size(n: usize) -> bool { n >= 1 }

Try / catch

// Panic is internal; validate before constructing:
if worker_count == 0 {
    return Err(io::Error::new(io::ErrorKind::InvalidInput, "need >= 1 worker"));
}
let pool = LocalPoolHandle::new(worker_count);

Prevention

When it happens

Trigger: Calling spawn_pinned on a LocalPoolSet built with zero worker threads; calling after all workers were removed; constructing the pool with an empty worker list.

Common situations: Misconfiguration when building the spawn_pinned pool (worker count 0); shutdown that emptied the pool before a late spawn; misuse of internal APIs that allow empty pools.

Related errors


AI-assisted analysis of tokio-rs/tokio@625954f365 (2026-08-11). Data as JSON: /api/errors/fded608e63f561b7. Report an issue: GitHub.