tokio-rs/tokio · error
A Tokio 1.x context was found, but it is being shutdown.
Error message
A Tokio 1.x context was found, but it is being shutdown.
What it means
This panic comes from Tokio's timer when code attempts to register a timer (create a Sleep/Interval or drive timer entries) while the Tokio runtime is in the middle of shutting down. A Tokio 1.x context was found on the thread, but it is marked TempLocalContext::Shutdown, so the timer's registration queue can no longer accept new entries and the library panics with RUNTIME_SHUTTING_DOWN_ERROR instead of silently misbehaving. It is a library invariant check: timers must never be created or registered after shutdown has begun.
Solutions
- Ensure the Runtime outlives every timer user: keep the Runtime value alive until all tasks/futures using tokio::time have completed, and drop timer-holding values before calling shutdown.
- Use runtime.shutdown_timeout(Duration) or shutdown_background instead of a bare drop/shutdown so pending tasks get drained and stop calling timer APIs mid-teardown.
- Remove time API calls (sleep, interval, Timeout) from Drop implementations and thread-local destructors; defer cleanup work to a spawned task before shutdown.
- Do not call block_on on a runtime after shutdown has started; spawn post-shutdown work on a different runtime or executor.
- If work must continue past runtime shutdown, clone a tokio::runtime::Handle and use it on a runtime that is guaranteed alive, or run the work with std::thread::sleep instead of tokio::time::sleep.
Example fix
// before: runtime dropped/shutdown while a spawned task still sleeps
let rt = tokio::runtime::Runtime::new().unwrap();
rt.spawn(async { tokio::time::sleep(Duration::from_secs(10)).await; });
drop(rt); // shutdown begins; task polls sleep -> panic: context is being shutdown
// after: give tasks time to finish before teardown
let rt = tokio::runtime::Runtime::new().unwrap();
rt.spawn(async { tokio::time::sleep(Duration::from_secs(10)).await; });
rt.shutdown_timeout(Duration::from_secs(15)); // drains tasks, no panic Defensive patterns
Strategy: try-catch
Validate before calling
// Rust: this is a panic, not a Result; ensure a live runtime context before timer use let handle = tokio::runtime::Handle::try_current(); assert!(handle.is_ok(), "timer APIs require a live Tokio runtime context"); // and do not create timers after shutdown: keep the Runtime alive while // any future using tokio::time is polled.
Try / catch
// Panics are not catchable with normal error handling; guard with catch_unwind only as a last resort
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
rt.block_on(async { tokio::time::sleep(std::time::Duration::from_secs(1)).await; })
}));
if result.is_err() {
eprintln!("timer used during runtime shutdown");
} Prevention
- Keep the Runtime alive until all tasks that use tokio::time have finished; prefer shutdown_timeout over drop.
- Never call tokio::time APIs (sleep, interval, Timeout) from Drop impls or thread-local destructors.
- Clone the runtime Handle into background tasks so ownership makes runtime lifetime explicit.
- Do not reuse a Runtime with block_on after calling shutdown/shutdown_background.
- Increase shutdown_timeout if tasks perform timed waits during teardown.
When it happens
Trigger: Creating a timer primitive (tokio::time::sleep, sleep_until, interval, Timeout, timer EntryHandle::new) or otherwise entering the timer driver when the thread-local context is TempLocalContext::Shutdown — i.e. after Runtime::shutdown or during runtime teardown. Typical concrete calls: tokio::time::sleep(...) or a future containing it being polled/spawned from within a Drop impl, a thread-local destructor, or block_on executed after the runtime has begun shutting down.
Common situations: Dropping values that call sleep/cancel timers in their Drop impls while the runtime shuts down; spawning or awaiting timers from tokio::task::block_in_place or from a plain std thread using a Handle whose runtime is shutting down; calling runtime.block_on() again after shutdown_timeout/shutdown_background; holding a Runtime in a static or thread-local that is torn down after the runtime; ordering bugs where a shutdown_timeout is too short and tasks still call time APIs during teardown.
Related errors
- Failed to parse value of
- Failed to parse value of
- Failed to parse value of
- Failed to parse value of
- Failed to parse value of
AI-assisted analysis of tokio-rs/tokio@8146318256 (2026-09-14).
Data as JSON: /api/errors/7d902e064639b628.
Report an issue: GitHub.
Appendix: source
Thrown at tokio/src/runtime/time_alt/timer.rs:37
}
impl Drop for Timer {
fn drop(&mut self) {
self.entry.cancel();
}
}
impl Timer {
#[track_caller]
pub(crate) fn new(handle: scheduler::Handle, deadline: u64) -> Self {
let entry = with_current_temp_local_context(&handle, |ctx| match ctx {
Some(TempLocalContext::Running { registration_queue }) => {
let entry = EntryHandle::new(deadline);
unsafe { registration_queue.push_front(entry.clone()) }
entry
}
#[cfg(feature = "rt-multi-thread")]
Some(TempLocalContext::Shutdown) => panic!("{RUNTIME_SHUTTING_DOWN_ERROR}"),
_ => {
let entry = EntryHandle::new(deadline);
push_from_remote(&handle, entry.clone());
entry
}
});
Timer { entry }
}
pub(crate) fn is_elapsed(&self) -> bool {
self.entry.is_woken_up()
}
pub(crate) fn poll_elapsed(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
self.entry.poll(cx)
}View on GitHub (pinned to 8146318256)