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
- Construct the LocalPoolSet with at least one worker thread.
- Guard against spawning after the pool has been shut down/emptied.
- Add an assertion at pool construction time rejecting worker_count == 0.
- 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
- Construct LocalPoolHandle with at least one worker.
- Keep the pool alive across all spawn_pinned calls.
- Don't spawn after pool shutdown.
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
- Unhandled Panic behavior modified after starting LocalSet
- there is no signal driver running, must be called from the c
- provided length would overflow after adjustment
- task panicked
- `JoinError` reason is not a panic.
AI-assisted analysis of tokio-rs/tokio@625954f365 (2026-08-11).
Data as JSON: /api/errors/fded608e63f561b7.
Report an issue: GitHub.