← Week 1: Tokio Fundamentals

Day 5: Timers, select!, and Cancellation

Phase 2 · Jun 14, 2026

← Week 1: Tokio Fundamentals

Agenda (2–3 hours)

  • Read (45 min): tokio::time and tokio::select! documentation; Tokio cancellation safety guide
  • Study (45 min): Which futures are select!-safe? What state is lost when a future is dropped mid-await?
  • Practice (45 min): Build a server that accepts a connection and times it out after 30s of inactivity; use select! to race the connection and a timer
  • Challenge (30 min): Implement a debounce function: given a stream of events, emit only the last event if no new event arrives within a 100ms window
← Week 1: Tokio Fundamentals

tokio::time

use tokio::time::{sleep, timeout, interval, Duration, Instant};

// One-shot delay
sleep(Duration::from_millis(100)).await;

// Deadline-based timeout on any future
let result = timeout(Duration::from_secs(5), some_future()).await?;

// Repeating timer
let mut ticker = interval(Duration::from_secs(1));
loop {
    ticker.tick().await;
    println!("tick");
}

Tokio's timer wheel is O(1) for insertions and expirations — far more efficient than creating OS timer threads.

← Week 1: Tokio Fundamentals

tokio::select!

select! polls multiple futures concurrently; completes when the first one finishes.

tokio::select! {
    result = operation_one() => { handle_one(result) }
    result = operation_two() => { handle_two(result) }
    _ = shutdown_signal() => { break }
}

On completion of one branch, all other branches are dropped (their futures are cancelled).

// Classic: connection with timeout
tokio::select! {
    _ = tokio::time::sleep(Duration::from_secs(30)) => {
        eprintln!("connection timed out");
    }
    result = handle_connection(stream) => {
        if let Err(e) = result { eprintln!("error: {}", e); }
    }
}
← Week 1: Tokio Fundamentals

Cancellation Safety

When a future is dropped (e.g., as a losing branch of select!), it's cancelled at the last await point.

Cancellation-safe: the operation can be safely restarted without side effects

  • TcpStream::read() — safe: no bytes were consumed if not completed
  • mpsc::Receiver::recv() — safe: message remains in queue

NOT cancellation-safe: AsyncWriteExt::write_all() — if cancelled mid-write, the connection is in an unknown state; don't use write_all() in a select! branch.

Use tokio_util::io::ReaderStream for cancellation-safe streaming reads.

← Week 1: Tokio Fundamentals

Graceful Shutdown Pattern

use tokio::signal;

let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel::<()>(1);

// In main
tokio::select! {
    _ = signal::ctrl_c() => { shutdown_tx.send(()).unwrap(); }
    _ = server_loop(shutdown_rx) => {}
}

// In server loop
async fn server_loop(mut shutdown: broadcast::Receiver<()>) {
    loop {
        tokio::select! {
            conn = listener.accept() => { /* handle */ }
            _ = shutdown.recv() => break,
        }
    }
}
← Week 1: Tokio Fundamentals

Key Takeaways

  • sleep, timeout, interval cover most timer needs; Tokio's timer wheel is O(1)
  • select! races futures; the losers are dropped (cancelled) immediately
  • Check cancellation safety before using a future in select! — some operations leave resources in inconsistent state
  • Broadcast channel + select! is the standard graceful shutdown pattern

Tomorrow: JoinSet, structured concurrency, and CancellationToken.