tokio-rs/tokio · error · io::Error
blocking pool shutting down
Error message
blocking pool shutting down
What it means
SpawnError::ShuttingDown surfaced as an io::Error with kind Other. It is produced when the runtime's blocking thread pool (used by spawn_blocking and internal blocking ops) has already begun shutdown and therefore refuses to schedule a new blocking task. The From<SpawnError> impl converts it into the generic io::Error shown.
Source
Thrown at tokio/src/runtime/blocking/pool.rs:208
allow(dead_code)
)]
Mandatory,
NonMandatory,
}
pub(crate) enum SpawnError {
/// Pool is shutting down and the task was not scheduled
ShuttingDown,
/// There are no worker threads available to take the task
/// and the OS failed to spawn a new one
NoThreads(io::Error),
}
impl From<SpawnError> for io::Error {
fn from(e: SpawnError) -> Self {
match e {
SpawnError::ShuttingDown => {
io::Error::new(io::ErrorKind::Other, "blocking pool shutting down")
}
SpawnError::NoThreads(e) => e,
}
}
}
impl Task {
pub(crate) fn new(task: task::UnownedTask<BlockingSchedule>, mandatory: Mandatory) -> Task {
Task { task, mandatory }
}
pub(super) fn shutdown(self) {
self.task.shutdown();
}
pub(super) fn run(self) {
self.task.run();
}View on GitHub (pinned to 7d0d729d8f)
Solutions
- Ensure spawn_blocking calls happen while the runtime is still alive — structure shutdown so blocking tasks drain before Runtime::drop.
- Keep a runtime guard (Runtime) alive for the full duration blocking work may be submitted.
- Use Handle::is_alive / handle the Err from spawn_blocking to degrade gracefully.
- Avoid submitting blocking work from Drop impls that may run during teardown.
Example fix
// before
let handle = runtime.handle().clone();
drop(runtime);
handle.spawn_blocking(|| { /* ... */ }); // fails
// after
let jh = runtime.spawn_blocking(|| { /* ... */ });
jh.await?; // drain before dropping runtime
drop(runtime); Defensive patterns
Strategy: validation
Validate before calling
// Validate the runtime/driver is alive before spawning blocking work.
if handle.runtime_flavor() == RuntimeFlavor::CurrentThread && /* runtime dropped */ {
return Err(io::Error::new(io::ErrorKind::Other, "runtime gone"));
}
// Prefer: spawn early and hold the JoinHandle. Try / catch
match runtime.spawn_blocking(|| expensive()) {
_ => {} // spawn_blocking itself returns a JoinHandle, not Result; the error surfaces on .await
}
// On .await:
match jh.await {
Ok(v) => v,
Err(e) if e.to_string().contains("shutting down") => return,
Err(e) => return Err(e.into()),
} Prevention
- Own the Runtime for the full lifetime of any blocking task.
- Drain blocking JoinHandles before dropping the runtime.
- Avoid spawn_blocking from Drop impls.
When it happens
Trigger: Calling spawn_blocking (or any API that schedules onto the blocking pool, e.g. some fs operations after shutdown) after the runtime has been dropped or shut down; spawning blocking work from a task whose runtime handle outlives the pool's shutdown.
Common situations: Storing a Handle and using it after Runtime::drop; spawning blocking work from a Drop impl or a background thread that races runtime teardown; using #[tokio::main] and awaiting work past the function's runtime lifetime.
Related errors
- background task failed
- background task failed
- A Tokio 1.x context was found, but it is being shutdown.
- A Tokio 1.x context was found, but it is being shutdown.
- signal driver gone
AI-assisted analysis of tokio-rs/tokio@7d0d729d8f (2026-08-11).
Data as JSON: /api/errors/d6fc95569561daa1.
Report an issue: GitHub.