Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

Recipe

Test a timeout or a delay without waiting for it

Use when: async code waits, with a timeout, a sleep or a pause between retries, and a test must prove how long it waits without taking that long

Goal: a #[tokio::test(start_paused = true)] test that runs in no real time and checks each duration to the millisecond

Taught in Testing async code

  1. 1

    Mark the test #[tokio::test(start_paused = true)]. Turn on Tokio's test-util feature in the dev-dependencies.

    Example

    tokio = { version = "1", features = ["full", "test-util"] } under [dev-dependencies], then #[tokio::test(start_paused = true)] above async fn a_quick_quote_arrives().

    Why: with the clock paused, Tokio jumps to the next timer whenever every task is waiting.

  2. 2

    Check that each wait in the code goes through tokio::time, such as sleep or timeout.

    Example

    quote_or_give_up waits with sleep(answer_after) inside timeout(limit, ...). Both are Tokio timers, so even an hour-long sleep costs no real time.

    • the wait is tokio::time::sleep or timeout — the paused clock skips it.
    • the wait is std::thread::sleep, a long loop or a spawn_blocking job — it takes real time, so replace it with a fake that calls tokio::time::sleep.

    Why: the paused clock only controls Tokio's own timers, and it stops skipping while blocking work runs.

  3. 3

    Write let start = tokio::time::Instant::now(); before the call, and read start.elapsed() after it.

    Example

    With answer_after set to 3600 seconds and limit to 1 second, start.elapsed() comes out as Duration::from_secs(1).

    Why: std::time::Instant reads the real clock, which barely moves during a paused test.

  4. 4

    Decide whether the test checks only the end, or a moment in between.

    Example

    The reminder test spawns remind_after with 600 seconds and yields. It advances 599 seconds and checks that nothing has arrived. Then it advances 1 more second and receives the message.

    • only the end — .await the call, and auto-advance jumps to each timer.
    • a moment between timers — spawn the task, call tokio::task::yield_now().await, then advance(...) to that moment.

    Why: a spawned task doesn't run until the test yields, so without yield_now its timer starts late.

  5. 5

    Compare start.elapsed() with the sum of the waits, using assert_eq!. Wrap any wait that might never end in a timeout, so a bug fails the test instead of hanging it.

    Example

    assert_eq!(start.elapsed(), Duration::from_secs(600)); in the reminder test. On the paused clock, 600 seconds means 600, not 600.002.

    Why: pretend time never runs late, so a wrong wait shows up as a different number, not as a flaky test.

Common mistakes

  • Measuring with std::time::Instant. In a paused test it shows almost no time passing.
  • Calling advance straight after tokio::spawn, before the task has started its timer.
  • Leaving real blocking work in a timing test. The clock stops skipping while it runs.
  • Putting a plain #[test] on an async fn. It doesn't compile.