Timers & Intervals
Tokio's timer wheel drives sleep, interval, timeout, and Instant-based scheduling without blocking worker threads. Enable the time feature for production services.
Recipe
Quick-reference recipe card - copy-paste ready.
use tokio::time::{sleep, Duration, timeout};
async fn work() -> &'static str {
sleep(Duration::from_millis(50)).await;
"ok"
}
#[tokio::main]
async fn main() {
let result = timeout(Duration::from_secs(1), work()).await;
println!("{:?}", result);
}When to reach for this: Debouncing, periodic polls, request deadlines, retry backoff, and heartbeat ticks.
Working Example
use tokio::time::{interval, sleep, Duration, MissedTickBehavior};
#[tokio::main]
async fn main() {
let mut ticker = interval(Duration::from_millis(100));
ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
for _ in 0..3 {
ticker.tick().await;
println!("tick");
}
sleep(Duration::from_millis(250)).await;
ticker.tick().await; // skips missed ticks with Skip behavior
println!("resumed");
}What this demonstrates:
intervalproduces periodic wakes.MissedTickBehavior::Skipavoids catch-up storm after delay.sleepis one-shot delay.
Deep Dive
How It Works
- Timer wheel: Efficient scheduling of many deadlines.
- sleep: Relative delay from now.
- interval: Periodic; first tick completes immediately unless
interval_atused. - timeout: Wrapper cancelling inner future on expiry.
Missed Tick Behavior
| Mode | Behavior |
|---|---|
Burst | Catch up all missed ticks |
Delay | Delay next tick by overrun time |
Skip | Reset schedule after overrun |
Rust Notes
use tokio::time::{Instant, Duration};
let start = Instant::now() + Duration::from_secs(1);
let mut ticker = tokio::time::interval_at(start, Duration::from_secs(5));- Use
Instantfromtokio::time, notstd::time::Instant, in async tests with pause. - Layer timeouts: connect, TTFB, total request.
- Exponential backoff:
sleep(duration * 2u32.pow(attempt))with cap.
Gotchas
- std::thread::sleep in async - Blocks worker. Fix:
tokio::time::sleep. - interval catch-up storm - Default burst fires many ticks after stall. Fix:
MissedTickBehavior::Skipfor heartbeats. - timeout too tight on cold start - False failures on JIT/connection warmup. Fix: Separate warmup budget or retry.
- Timer without time feature - Compile/runtime failure. Fix:
features = ["time"]on tokio. - Assuming wall-clock precision - Timers are cooperative under load. Fix: Do not use for hard real-time guarantees.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
cron / external scheduler | Cluster-wide jobs | In-process per-request delay |
sleep in blocking thread | Sync CLI | Tokio worker context |
tokio-cron-scheduler | Periodic maintenance tasks | Sub-millisecond timing |
| Busy poll | Never | Never in Tokio services |
FAQs
sleep vs interval?
sleep once; interval repeats until dropped.
timeout error type?
Elapsed from tokio::time::error::Elapsed - inner future is dropped.
Testing timers?
#[tokio::test(start_paused = true)] then advance for deterministic time.
interval first tick immediate?
Yes by default - use interval_at to align first fire.
Precision under load?
Timers fire after deadline when runtime polls - not real-time OS guarantees.
reqwest timeout?
Client-level timeout complements Tokio timeout wrapper - configure both thoughtfully.
backoff crate?
backoff or manual exponential sleep for retries with jitter.
watchdog pattern?
select! between work and sleep(deadline) branch for stall detection.
pause in production?
No - pause is test-only API on test runtime.
DelayQueue?
tokio_util::time::DelayQueue for many keyed deadlines - timers for streams of expiring items.
Related
- Cancellation & Timeouts - deadlines
- Combining Futures - timeout in select
- select! & Concurrency - timer branches
- Tokio Basics - sleep intro
- Graceful Shutdown - drain timeouts
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+.