Cancellation & Timeouts
In Rust async, cancellation is cooperative: dropping a future or calling JoinHandle::abort stops work at the next await point. Combine timeouts and shutdown signals so services fail fast and shut down cleanly.
Search across all documentation pages
In Rust async, cancellation is cooperative: dropping a future or calling JoinHandle::abort stops work at the next await point. Combine timeouts and shutdown signals so services fail fast and shut down cleanly.
Quick-reference recipe card - copy-paste ready.
use tokio::time::{timeout, Duration};
async fn work() -> &'static str {
"done"
}
#[tokio::main]
async fn main() {
match timeout(Duration::from_secs(1), work()).await {
Ok(v) => println!("{v}"),
Err(_) => println!("timed out"),
}
}When to reach for this: HTTP client deadlines, graceful shutdown, request-scoped work limits, and preventing hung tasks.
use tokio::sync::watch;
use tokio::time::{sleep, Duration};
async fn worker(mut shutdown: watch::Receiver<bool>) {
loop {
tokio::select! {
_ = shutdown.changed() => {
if *shutdown.borrow() {
println!("shutting down");
break;
}
}
_ = sleep(Duration::from_millis(100)) => {
println!("tick");
}
}
}
}
#[tokio::main]
async fn main() {
let (tx, rx) = watch::channel(false);
let handle = tokio::spawn(worker(rx));
sleep(Duration::from_millis(250)).await;
tx.send(true).unwrap();
handle.await.unwrap();
}What this demonstrates:
watch channel broadcasts shutdown flag.select! races work ticks against shutdown.abort: JoinHandle::abort cancels spawned tasks similarly.timeout: Wraps a future; on expiry the inner future is dropped.tokio_util::sync::CancellationToken propagates cancel to child tasks.| Tool | Scope |
|---|---|
| Drop future | Single await chain |
handle.abort() | Spawned task |
timeout / sleep | Time-bounded operations |
CancellationToken | Tree of related tasks |
use tokio_util::sync::CancellationToken;
async fn scoped(token: CancellationToken) {
let child = token.child_token();
let _guard = token.drop_guard();
tokio::select! {
_ = token.cancelled() => {}
_ = do_work(child) => {}
}
}Drop impls or defer patterns when cancel must release resources.select! loops.yield_now().await or check cancel token in loops.scopeguard, explicit finally async patterns, or CancellationToken with cleanup branch.biased and read Tokio docs; buffer or use watch/broadcast appropriately.Err - do not treat as failure. Fix: Match is_cancelled() on error.| Alternative | Use When | Don't Use When |
|---|---|---|
timeout only | Single operation deadline | Process-wide shutdown |
watch / broadcast | Global shutdown signal | Per-request cancel only |
CancellationToken | Parent-child task trees | One-shot timeout |
| Process kill | Hung unsafe code | Normal async cleanup path |
Yes when futures are dropped, but work runs until the next .await unless you poll cancel tokens in tight loops.
Handler future is dropped - design idempotent side effects or use detached tasks with care.
timeout cancels wrapped work; sleep only fires a timer branch.
CancellationToken is designed for cancel trees; watch is a simple bool snapshot for shutdown.
Dropping fetch future cancels in-flight query on supported drivers - verify for your DB.
Set Client::builder().timeout(...) plus per-request overrides for layered deadlines.
Spawn task, abort, assert cleanup flags; use tokio::time::pause for timeouts.
Graceful drains in-flight work; immediate abort stops at next await - pick per deploy strategy.
Release locks in Drop or explicit cleanup branch to avoid poisoned state.
Parent awaits all children - dropping parent cancels children when using scoped task patterns.
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.
Reviewed by Chris St. John·Last updated Jul 16, 2026