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.
Recipe
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.
Working Example
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:
watchchannel broadcasts shutdown flag.select!races work ticks against shutdown.- Task exits loop cooperatively when cancelled.
Deep Dive
How It Works
- Drop = cancel: Dropping an un-awaited future cancels in-flight work at await boundaries.
abort:JoinHandle::abortcancels spawned tasks similarly.timeout: Wraps a future; on expiry the inner future is dropped.- Shutdown tokens:
tokio_util::sync::CancellationTokenpropagates cancel to child tasks.
Cancellation Tools
| Tool | Scope |
|---|---|
| Drop future | Single await chain |
handle.abort() | Spawned task |
timeout / sleep | Time-bounded operations |
CancellationToken | Tree of related tasks |
Rust Notes
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) => {}
}
}- Run cleanup in
Dropimpls ordeferpatterns when cancel must release resources. - Document which operations are cancel-safe in
select!loops. - Axum request handlers cancel when clients disconnect (future dropped).
Gotchas
- Assuming immediate stop - CPU-bound loop without await ignores cancellation. Fix: Insert
yield_now().awaitor check cancel token in loops. - Lost cleanup on drop - DB transactions left open. Fix:
scopeguard, explicitfinallyasync patterns, orCancellationTokenwith cleanup branch. - timeout too aggressive - Kills slow but valid queries. Fix: Tier timeouts (connect vs read vs total) and retry idempotent ops.
- select! non-cancel-safe recv - Dropping branch may lose messages. Fix: Use
biasedand read Tokio docs; buffer or usewatch/broadcastappropriately. - Ignoring JoinError after abort - Expected
Err- do not treat as failure. Fix: Matchis_cancelled()on error.
Alternatives
| 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 |
FAQs
Is cancellation automatic?
Yes when futures are dropped, but work runs until the next .await unless you poll cancel tokens in tight loops.
Client disconnect in Axum?
Handler future is dropped - design idempotent side effects or use detached tasks with care.
timeout vs sleep in select?
timeout cancels wrapped work; sleep only fires a timer branch.
CancellationToken vs watch?
CancellationToken is designed for cancel trees; watch is a simple bool snapshot for shutdown.
sqlx query cancel?
Dropping fetch future cancels in-flight query on supported drivers - verify for your DB.
reqwest timeout?
Set Client::builder().timeout(...) plus per-request overrides for layered deadlines.
Testing cancellation?
Spawn task, abort, assert cleanup flags; use tokio::time::pause for timeouts.
Graceful vs immediate?
Graceful drains in-flight work; immediate abort stops at next await - pick per deploy strategy.
Mutex on cancel path?
Release locks in Drop or explicit cleanup branch to avoid poisoned state.
Structured concurrency?
Parent awaits all children - dropping parent cancels children when using scoped task patterns.
Related
- Combining Futures - select and timeout
- Graceful Shutdown - production shutdown
- Timers & Intervals - sleep and interval
- Tasks & JoinHandles - abort
- Async Best Practices - cancel-safe design
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+.