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).
use std::time::Duration;
use tokio::time::sleep;
#[tokio::main]
async fn main() {
println!("before");
sleep(Duration::from_millis(20)).await;
println!("after");
}before
afterA 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.
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());
}elapsed: 1s
elapsed: 3sThe 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.
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));
}fast woke at 100 ms
middle woke at 200 ms
slow woke at 300 ms
virtual time: 3600 s
real time under 5 s: trueThe 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.
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());
}after 1 microsecond: 1ms
after 1500 microseconds more: 3msA 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.
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());
}step 1 done at 100 ms
deadline reached at 250 ms
past deadline, still at 250 msThe 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.
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}");
}
}fast: Ok("found")
slow: Err(Elapsed(()))
error text: deadline has elapsedElapsed 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.
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());
}worker: started
worker state dropped
result is_err = trueWork 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.
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:?}");
}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.
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());
}
}tick 1 at 0 ms
tick 2 at 100 ms
tick 3 at 200 ms
tick 4 at 300 mstick 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.
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:?}");
}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.
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);
}
}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.
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());
}first tick at 50 ms
second tick at 150 ms
tick after reset at 290 ms
next tick at 390 msWithout 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.
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);
}
}
}
}event at 10 ms
event at 30 ms
idle timeout at 80 msThe 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.
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();
}at 9s: done = 0
at 10s: done = 0
at 10s: done = 1The 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).awaitwith auto-advance (or await theJoinHandle) overadvance. The documentation ofadvancerecommends this for forcing timeouts. - Use
advanceto move the clock while other tasks are idle, and let the tasks run (yield_nowor.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.
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);
}blocking job done at 0 ms
timer fired at 10 msThe 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.
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));
}
}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.00sThe 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.
#[tokio::main(flavor = "multi_thread")]
async fn main() {
tokio::time::pause();
}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
sleepyields the thread while it waits;std::thread::sleepblocks it. The delay starts whensleepis called, and nothing happens if the future is never awaited.- Tokio timers have millisecond granularity and round up.
timeoutreturnsResult<T, Elapsed>, drops the inner future on expiry, and cannot interrupt code that never reaches an.await.- The first
tickof an interval completes immediately. An interval keeps its schedule; a sleep loop adds the work time to each period. MissedTickBehavior:Burstcatches up with back-to-back ticks (default),Delayshifts the schedule,Skipdrops missed ticks. The behavior applies only when the tick is more than 5 ms late.Interval::tickis cancel safe.Sleep::resetchanges 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_threadand thetest-utilfeature. advancejumps the clock without waiting for the timers it passes;sleepwith auto-advance is the reliable way to wait for timers in tests.- A running
spawn_blockingjob inhibits auto-advance.
Summary
sleep,sleep_until,timeout,timeout_at,interval, andinterval_atbuild on one timer driver with 1 ms resolution.timeoutcancels the inner future by dropping it.Elapseddisplays asdeadline has elapsed.Intervalsupportsresetvariants 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;
advancemoves it by a fixed amount.
