Guide contents

Lesson 11 of 44

Time: sleep, timeout, and interval

tokio::time provides futures that complete after a delay, a wrapper that limits how long another future may take, and a repeating timer. This lesson also introduces the paused clock, a test facility of Tokio that replaces real waiting with instant jumps. Every timing example below uses it, so the output shows exact times.

Item Purpose
sleep(duration), sleep_until(instant) a future that completes at a deadline
Instant a point on the clock; tokio::time::Instant follows the paused clock
timeout(duration, future), timeout_at fail with Elapsed if the future is not done by the deadline
interval(period), interval_at(start, period) a repeating timer with tick().await
MissedTickBehavior what an interval does after a late tick: Burst, Delay, Skip
Sleep::reset move the deadline of an existing sleep
start_paused, time::pause, time::advance control the clock in tests and examples

These items need Tokio's time feature. The full feature includes it. The paused clock needs the test-util feature, which full does not include. Add it with cargo add tokio --features full,test-util or list it under [dev-dependencies]. Your First Tokio Program explains feature flags.

Example 1: sleep

sleep(duration) returns a future that completes after the duration. While it waits, the task uses no thread; the runtime runs other tasks. It is the async counterpart of std::thread::sleep, which blocks the thread (Blocking and CPU-Bound Work shows the difference).

Rust
use std::time::Duration;
use tokio::time::sleep;

#[tokio::main]
async fn main() {
    println!("before");
    sleep(Duration::from_millis(20)).await;
    println!("after");
}
Output
before
after

A Sleep does nothing until it is polled, like any future. The delay counts from the moment sleep is called, not from the first .await: sleep(d) is equivalent to sleep_until(Instant::now() + d), as the source documents. A sleep that is never awaited does not delay anything.

Example 2: Instant and the paused clock

tokio::time::Instant is a point on Tokio's clock. start.elapsed() returns the Duration since start. With the real clock the value differs on every run, so it cannot appear in verified output. The attribute start_paused = true on #[tokio::main] starts the runtime with the clock paused: the clock stops moving by itself. Pausing requires the current_thread flavor.

Rust
use std::time::Duration;
use tokio::time::{sleep, Instant};

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
    let start = Instant::now();
    sleep(Duration::from_secs(1)).await;
    println!("elapsed: {:?}", start.elapsed());
    sleep(Duration::from_secs(2)).await;
    println!("elapsed: {:?}", start.elapsed());
}
Output
elapsed: 1s
elapsed: 3s

The program did not wait three real seconds. The next example explains why the clock moved by exactly the sleep time.

Example 3: Auto-advance

When the clock is paused and the runtime has no task ready to run, the runtime moves the clock forward to the next pending timer and continues. This is called auto-advance. The documentation of time::pause states it: "If time is paused and the runtime has no work to do, the clock is auto-advanced to the next pending timer."

The implementation is in runtime/time/mod.rs. When every task is waiting, the time driver looks up the earliest timer deadline in its timer wheel. With the test-util feature, park_thread_timeout checks clock.can_auto_advance(), which is true when the clock is paused and no auto-advance inhibitor is active. The driver then checks for pending wake-ups without waiting (a zero-length park). If nothing woke the driver, it calls clock.advance(duration) for the time until the next deadline instead of sleeping the thread. The timers that are now due fire, their tasks become ready, and the runtime runs them.

The program shows the effect. Three tasks sleep for different times, and the last line compares the virtual and real time.

Rust
use std::time::Duration;
use tokio::time::{sleep, Instant};

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
    let start = Instant::now();
    let real_start = std::time::Instant::now();

    let mut handles = Vec::new();
    for (name, ms) in [("slow", 300), ("fast", 100), ("middle", 200)] {
        handles.push(tokio::spawn(async move {
            sleep(Duration::from_millis(ms)).await;
            println!("{name} woke at {} ms", start.elapsed().as_millis());
        }));
    }
    for handle in handles {
        handle.await.unwrap();
    }

    sleep(Duration::from_secs(3600)).await;
    println!("virtual time: {} s", start.elapsed().as_secs());
    println!("real time under 5 s: {}", real_start.elapsed() < Duration::from_secs(5));
}
Output
fast woke at 100 ms
middle woke at 200 ms
slow woke at 300 ms
virtual time: 3600 s
real time under 5 s: true

The tasks woke in deadline order, and each saw its exact deadline. One virtual hour passed in far less than five real seconds. std::time::Instant is the standard library's clock. It ignores the pause, which is why the program can use it to measure real time.

Example 4: Timers have millisecond granularity

The time driver rounds durations up to whole milliseconds. The sleep documentation says Sleep operates at millisecond granularity, and the source comment in park_internal says the driver rounds up to 1 ms to avoid microsecond-length OS sleeps. Under the paused clock this is visible.

Rust
use std::time::Duration;
use tokio::time::{sleep, Instant};

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
    let start = Instant::now();

    sleep(Duration::from_micros(1)).await;
    println!("after 1 microsecond: {:?}", start.elapsed());

    sleep(Duration::from_micros(1500)).await;
    println!("after 1500 microseconds more: {:?}", start.elapsed());
}
Output
after 1 microsecond: 1ms
after 1500 microseconds more: 3ms

A 1 microsecond sleep took a full millisecond, and a 1.5 millisecond sleep took 2 milliseconds (1 ms + 2 ms = 3 ms in total). Timers are not suitable for sub-millisecond delays.

Example 5: sleep_until

sleep_until(deadline) completes when the clock reaches an Instant. A fixed deadline is useful when several steps must finish by a common time, because time spent in an earlier step reduces the remaining wait. An Instant plus a Duration gives a later Instant.

Rust
use std::time::Duration;
use tokio::time::{sleep, sleep_until, Instant};

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
    let start = Instant::now();
    let deadline = start + Duration::from_millis(250);

    sleep(Duration::from_millis(100)).await;
    println!("step 1 done at {} ms", start.elapsed().as_millis());

    sleep_until(deadline).await;
    println!("deadline reached at {} ms", start.elapsed().as_millis());

    sleep_until(deadline).await;
    println!("past deadline, still at {} ms", start.elapsed().as_millis());
}
Output
step 1 done at 100 ms
deadline reached at 250 ms
past deadline, still at 250 ms

The second sleep_until waited only the remaining 150 ms. The third returned at once because the deadline had passed.

Example 6: timeout

timeout(duration, future) returns a future that resolves to Result<T, Elapsed>. If the inner future finishes first, the result is Ok(value). If the duration passes first, the result is Err(Elapsed) and the inner future is dropped, which cancels it (Cancellation, abort, and Panics explains cancellation). Elapsed is the error type in tokio::time::error. Its Display text is deadline has elapsed.

Rust
use std::time::Duration;
use tokio::time::{sleep, timeout};

async fn slow_lookup(ms: u64) -> &'static str {
    sleep(Duration::from_millis(ms)).await;
    "found"
}

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
    let fast = timeout(Duration::from_millis(100), slow_lookup(20)).await;
    println!("fast: {fast:?}");

    let slow = timeout(Duration::from_millis(100), slow_lookup(500)).await;
    println!("slow: {slow:?}");
    if let Err(e) = slow {
        println!("error text: {e}");
    }
}
Output
fast: Ok("found")
slow: Err(Elapsed(()))
error text: deadline has elapsed

Elapsed implements std::error::Error, so ? can convert it into a Box<dyn Error>. It also converts into std::io::Error with kind TimedOut.

Example 7: A timeout cancels the inner future

When the timeout expires, the inner future is dropped at its current .await. Values it holds are dropped too. A guard type with a Drop implementation shows the moment of cancellation.

Rust
use std::time::Duration;
use tokio::time::{sleep, timeout};

struct Guard(&'static str);

impl Drop for Guard {
    fn drop(&mut self) {
        println!("{} dropped", self.0);
    }
}

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
    let result = timeout(Duration::from_millis(50), async {
        let _guard = Guard("worker state");
        println!("worker: started");
        sleep(Duration::from_secs(10)).await;
        println!("worker: never printed");
    })
    .await;

    println!("result is_err = {}", result.is_err());
}
Output
worker: started
worker state dropped
result is_err = true

Work done before the last .await is not rolled back. A timeout on a future that already sent a request has cancelled the wait for the reply, not the request.

Example 8: A timeout cannot interrupt code that does not yield

timeout checks the deadline when the inner future returns Pending. The timeout documentation says the inner future is polled before the timeout is checked, so a future that finishes in its first poll succeeds even when it took longer than the duration. Blocking code has no .await, so the timeout has nothing to interrupt.

Rust
use std::time::Duration;
use tokio::time::timeout;

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
    let result = timeout(Duration::from_millis(10), async {
        std::thread::sleep(Duration::from_millis(50));
        "finished after 50 ms of blocking"
    })
    .await;

    println!("{result:?}");
}
Output
Ok("finished after 50 ms of blocking")

The result is Ok although the work took five times the limit. A timeout works only around code that reaches .await points. Blocking work belongs in spawn_blocking first.

Example 9: interval

interval(period) creates an Interval. tick().await waits for the next tick. The first tick completes immediately. Later ticks are spaced by period. The value returned by tick is the Instant the tick was scheduled for. interval panics if the period is zero.

Rust
use std::time::Duration;
use tokio::time::{interval, Instant};

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
    let start = Instant::now();
    let mut ticker = interval(Duration::from_millis(100));

    for n in 1..=4 {
        ticker.tick().await;
        println!("tick {n} at {} ms", start.elapsed().as_millis());
    }
}
Output
tick 1 at 0 ms
tick 2 at 100 ms
tick 3 at 200 ms
tick 4 at 300 ms

tick is cancel safe: if tick() is used in a select! branch and another branch wins, no tick is consumed (the method's documentation states this).

Example 10: A sleep loop drifts, an interval does not

A loop that does work and then sleeps repeats every work + sleep. The period grows by the work time. An Interval measures from the schedule, not from the end of the work, so the ticks stay at multiples of the period. Both loops below do 30 ms of work per round, modeled by a sleep.

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

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
    let work = Duration::from_millis(30);
    let period = Duration::from_millis(100);

    let start = Instant::now();
    let mut starts = Vec::new();
    for _ in 0..4 {
        starts.push(start.elapsed().as_millis());
        sleep(work).await;
        sleep(period).await;
    }
    println!("sleep loop starts:    {starts:?}");

    let start = Instant::now();
    let mut ticker = interval(period);
    let mut starts = Vec::new();
    for _ in 0..4 {
        ticker.tick().await;
        starts.push(start.elapsed().as_millis());
        sleep(work).await;
    }
    println!("interval loop starts: {starts:?}");
}
Output
sleep loop starts:    [0, 130, 260, 390]
interval loop starts: [0, 100, 200, 300]

The sleep loop runs every 130 ms. The interval loop runs every 100 ms, because after 30 ms of work the next tick is only 70 ms away. A sleep loop is the right shape when a fixed pause after the work is wanted, such as a retry delay.

Example 11: MissedTickBehavior

A tick is missed when the work between two tick() calls takes longer than the period. MissedTickBehavior selects what happens next. The interval only applies the behavior when the tick is more than 5 ms late; the source compares now > timeout + Duration::from_millis(5).

Variant Behavior after a late tick
Burst (default) the missed ticks fire back to back until the schedule is caught up
Delay the next tick is one full period after the late tick, and the schedule shifts
Skip missed ticks are dropped; the next tick is the next multiple of the period from the start

The program runs each behavior with a period of 100 ms and a 250 ms stall after the first tick, then records the time of each of six ticks.

Rust
use std::time::Duration;
use tokio::time::{interval, sleep, Instant, MissedTickBehavior};

async fn run(behavior: MissedTickBehavior) -> Vec<u128> {
    let start = Instant::now();
    let mut ticker = interval(Duration::from_millis(100));
    ticker.set_missed_tick_behavior(behavior);

    let mut times = Vec::new();
    for round in 0..6 {
        ticker.tick().await;
        times.push(start.elapsed().as_millis());
        if round == 0 {
            sleep(Duration::from_millis(250)).await;
        }
    }
    times
}

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
    for behavior in [
        MissedTickBehavior::Burst,
        MissedTickBehavior::Delay,
        MissedTickBehavior::Skip,
    ] {
        println!("{behavior:?}: {:?}", run(behavior).await);
    }
}
Output
Burst: [0, 250, 250, 300, 400, 500]
Delay: [0, 250, 350, 450, 550, 650]
Skip: [0, 250, 300, 400, 500, 600]

With Burst the ticks scheduled for 100 ms and 200 ms fire immediately at 250 ms, then the schedule continues at 300, 400, 500. With Delay every later tick is 100 ms after the previous one, so the schedule moves to 350, 450, 550, 650. With Skip the ticks at 100 ms and 200 ms are dropped, and the next tick is the multiple of 100 ms after 250 ms, which is 300 ms.

Use Burst when every tick must be processed eventually, Delay when the spacing between runs matters more than the schedule, and Skip when only the latest tick matters.

Example 12: interval_at and reset

interval_at(start, period) sets when the first tick happens. Its default missed-tick behavior is also Burst. Interval::reset() restarts the schedule so the next tick comes one period from now. It ignores MissedTickBehavior.

Rust
use std::time::Duration;
use tokio::time::{interval_at, sleep, Instant};

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
    let start = Instant::now();
    let mut ticker = interval_at(start + Duration::from_millis(50), Duration::from_millis(100));

    ticker.tick().await;
    println!("first tick at {} ms", start.elapsed().as_millis());
    ticker.tick().await;
    println!("second tick at {} ms", start.elapsed().as_millis());

    sleep(Duration::from_millis(40)).await;
    ticker.reset();
    ticker.tick().await;
    println!("tick after reset at {} ms", start.elapsed().as_millis());
    ticker.tick().await;
    println!("next tick at {} ms", start.elapsed().as_millis());
}
Output
first tick at 50 ms
second tick at 150 ms
tick after reset at 290 ms
next tick at 390 ms

Without the reset the third tick would have been at 250 ms. After the reset at 190 ms it came at 290 ms, one period after the reset. reset_immediately, reset_after, and reset_at are variants that change when the next tick happens.

Example 13: A resettable Sleep with pin!

sleep(...) can be created once, polled through a mutable reference, and moved to a new deadline with Sleep::reset. That avoids creating a new timer for each event. A Sleep must be pinned before it is polled by reference; tokio::pin! pins it on the stack (Polling, Wakers, and a Tiny Executor explains Pin, and select!: Racing Futures shows &mut future with pin!).

The program implements an idle timeout: each event pushes the deadline out by 50 ms, and the loop ends when 50 ms pass without an event. Events arrive after gaps of 10, 20, and 80 ms.

Rust
use std::time::Duration;
use tokio::time::{sleep, Instant};

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
    let idle_limit = Duration::from_millis(50);
    let start = Instant::now();

    let idle = sleep(idle_limit);
    tokio::pin!(idle);

    for gap_ms in [10u64, 20, 80] {
        tokio::select! {
            _ = &mut idle => {
                println!("idle timeout at {} ms", start.elapsed().as_millis());
                break;
            }
            _ = sleep(Duration::from_millis(gap_ms)) => {
                println!("event at {} ms", start.elapsed().as_millis());
                idle.as_mut().reset(Instant::now() + idle_limit);
            }
        }
    }
}
Output
event at 10 ms
event at 30 ms
idle timeout at 80 ms

The third event would have come at 110 ms. The idle deadline, moved to 80 ms by the second event, expired first.

Example 14: pause, advance, and the polling rule

#[tokio::main(start_paused = true)] is the usual entry point. tokio::time::pause() pauses the clock from inside a running current_thread runtime, and tokio::time::resume() returns to the real clock. pause panics on other runtimes. tokio::time::advance(duration) moves a paused clock forward by a fixed amount. It does not wait for timers: the documentation says all sleeps whose deadline lies before the new time complete "at the same time", and the runtime may poll them in any order.

Rust
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::time::{self, sleep, Instant};

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
    let start = Instant::now();
    let done = Arc::new(AtomicU32::new(0));
    let flag = done.clone();

    let task = tokio::spawn(async move {
        sleep(Duration::from_secs(10)).await;
        flag.fetch_add(1, Ordering::SeqCst);
    });
    tokio::task::yield_now().await;

    time::advance(Duration::from_secs(9)).await;
    println!("at {:?}: done = {}", start.elapsed(), done.load(Ordering::SeqCst));

    time::advance(Duration::from_secs(1)).await;
    println!("at {:?}: done = {}", start.elapsed(), done.load(Ordering::SeqCst));

    tokio::task::yield_now().await;
    println!("at {:?}: done = {}", start.elapsed(), done.load(Ordering::SeqCst));

    task.await.unwrap();
}
Output
at 9s: done = 0
at 10s: done = 0
at 10s: done = 1

The clock read 10 s right after the second advance, but the task had not yet run its next step. The advance documentation says a sleep is not guaranteed to complete on the first poll after advance, and that the runtime polls it again shortly. A spawned task first runs when the spawning task gives up the thread, so the first yield_now lets the task start its sleep, with a deadline of 10 s, before the clock moves. Two rules follow from this:

  • To wait for a timer to fire, prefer sleep(duration).await with auto-advance (or await the JoinHandle) over advance. The documentation of advance recommends this for forcing timeouts.
  • Use advance to move the clock while other tasks are idle, and let the tasks run (yield_now or .await) before checking their state.

Example 15: Blocking work and the paused clock

The paused clock knows only about Tokio timers. std::thread::sleep does not move it, and std::time::Instant ignores it. Auto-advance is also switched off while a spawn_blocking job runs: runtime/blocking/schedule.rs calls clock.inhibit_auto_advance() when a blocking task starts, and the pause documentation describes this as the way to keep the clock still while waiting for real I/O.

The program runs a 10 ms timer and a blocking job side by side.

Rust
use std::time::Duration;
use tokio::time::{sleep, Instant};

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
    let start = Instant::now();

    let timer = async {
        sleep(Duration::from_millis(10)).await;
        println!("timer fired at {} ms", start.elapsed().as_millis());
    };
    let blocking = async {
        tokio::task::spawn_blocking(|| std::thread::sleep(Duration::from_millis(100)))
            .await
            .unwrap();
        println!("blocking job done at {} ms", start.elapsed().as_millis());
    };

    tokio::join!(timer, blocking);
}
Output
blocking job done at 0 ms
timer fired at 10 ms

The 10 ms timer was due long before the 100 ms blocking job ended in real time, yet the job finished first. While the job ran, the clock could not auto-advance, so the timer stayed pending. After the job returned, the clock still read 0 ms, and the runtime then advanced it to the timer. The blocking sleep of 100 ms left no trace on the paused clock.

Example 16: Tests with start_paused

#[tokio::test(start_paused = true)] starts each test with a paused clock, so tests of timeouts and intervals run instantly and cannot be flaky. The fence flag rust,test makes the checker compile the block with --test and print the test report.

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

#[tokio::test(start_paused = true)]
async fn timeout_fires_after_the_limit() {
    let start = Instant::now();
    let result = timeout(Duration::from_secs(30), sleep(Duration::from_secs(60))).await;

    assert!(result.is_err());
    assert_eq!(start.elapsed(), Duration::from_secs(30));
}

#[tokio::test(start_paused = true)]
async fn interval_ticks_every_period() {
    let start = Instant::now();
    let mut ticker = interval(Duration::from_secs(5));
    for expected in [0, 5, 10] {
        ticker.tick().await;
        assert_eq!(start.elapsed(), Duration::from_secs(expected));
    }
}
Output
running 2 tests
test interval_ticks_every_period ... ok
test timeout_fires_after_the_limit ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

The first test waits 30 virtual seconds and checks the elapsed time to the nanosecond. Testing Async Code covers the full set of test tools.

Example 17: pause requires the current-thread runtime

Pausing the clock needs the current_thread runtime. Calling time::pause() on the multi-thread runtime panics.

Rustpanics at runtime
#[tokio::main(flavor = "multi_thread")]
async fn main() {
    tokio::time::pause();
}
Output
thread 'main' panicked at src/main.rs:3:5:
`time::pause()` requires the `current_thread` Tokio runtime. This is the default Runtime used by `#[tokio::test].

Interview points

  • sleep yields the thread while it waits; std::thread::sleep blocks it. The delay starts when sleep is called, and nothing happens if the future is never awaited.
  • Tokio timers have millisecond granularity and round up.
  • timeout returns Result<T, Elapsed>, drops the inner future on expiry, and cannot interrupt code that never reaches an .await.
  • The first tick of an interval completes immediately. An interval keeps its schedule; a sleep loop adds the work time to each period.
  • MissedTickBehavior: Burst catches up with back-to-back ticks (default), Delay shifts the schedule, Skip drops missed ticks. The behavior applies only when the tick is more than 5 ms late.
  • Interval::tick is cancel safe. Sleep::reset changes the deadline of an existing sleep and needs the sleep to be pinned.
  • With the clock paused and no ready task, the time driver advances the clock to the next timer. Pausing needs current_thread and the test-util feature.
  • advance jumps the clock without waiting for the timers it passes; sleep with auto-advance is the reliable way to wait for timers in tests.
  • A running spawn_blocking job inhibits auto-advance.

Summary

  • sleep, sleep_until, timeout, timeout_at, interval, and interval_at build on one timer driver with 1 ms resolution.
  • timeout cancels the inner future by dropping it. Elapsed displays as deadline has elapsed.
  • Interval supports reset variants and three missed-tick behaviors.
  • #[tokio::main(flavor = "current_thread", start_paused = true)] and #[tokio::test(start_paused = true)] make time-based code deterministic.
  • Auto-advance moves the paused clock to the next timer when the runtime is idle; advance moves it by a fixed amount.