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

  1. 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.
  2. 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.
  3. Remove time API calls (sleep, interval, Timeout) from Drop implementations and thread-local destructors; defer cleanup work to a spawned task before shutdown.
  4. Do not call block_on on a runtime after shutdown has started; spawn post-shutdown work on a different runtime or executor.
  5. 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

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


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)