Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
157: Tokio Time Utilities
You've likely spent plenty of time in the standard library using std::thread::sleep. It's intuitive: you tell the thread to stop everything and wait for a bit. But when you move into the world of Tokio, that intuition becomes a liability. I've seen too many developers drop a standard sleep call into an async function, see that the code "works" during local testing, and then watch their production throughput collapse under actual load.
Stopping the world with std::thread
Imagine you're writing a background worker that polls an external API for new messages every five seconds. In a synchronous world, you'd just use a loop and std::thread::sleep. If you do that inside a Tokio task, you aren't just pausing that one task; you're pausing the entire OS thread that the Tokio executor is using to run potentially hundreds of other tasks. Since Tokio uses a small pool of worker threads (usually equal to your CPU core count), blocking one of them is a huge deal. Block all of them, and your entire server stops responding, even though CPU usage is at 0%.
// DON'T DO THIS in an async function
async fn poll_api_naive() {
loop {
fetch_messages().await;
// This blocks the actual OS thread.
// No other tasks on this thread can progress!
std::thread::sleep(std::time::Duration::from_secs(5));
}
}
Letting the executor breathe
The better way is to use tokio::time::sleep. The key difference is that tokio::time::sleep doesn't block the thread; it returns a Future. When you .await it, you're telling the Tokio runtime, "I'm done for now; wake me up in five seconds." The executor then simply parks your task and immediately moves on to run other pending tasks on that same thread. It's the difference between standing in the middle of a doorway blocking everyone (std) and stepping aside to let people pass until your timer goes off (tokio).
use tokio::time::{sleep, Duration};
async fn poll_api_proper() {
loop {
fetch_messages().await;
// This yields control back to the runtime
sleep(Duration::from_secs(5)).await;
}
}
Handling the "forever" hang
Sleep is one thing, but what happens when the fetch_messages() call itself hangs? Maybe the remote server is experiencing a TCP timeout or a routing loop. If you just .await a future, you're at the mercy of that future's internal timeout logic—which might be thirty seconds, or nonexistent. I usually prefer to be explicit about how long I'm willing to wait for any single operation.
This is where tokio::time::timeout comes in. It wraps another future and returns a Result. If the inner future finishes first, you get Ok; if the duration expires first, you get an Err. It's a clean way to ensure a single stalled request doesn't hold onto resources indefinitely.
use tokio::time::{timeout, Duration};
async fn fetch_with_limit() {
let result = timeout(Duration::from_secs(2), fetch_messages()).await;
match result {
Ok(msgs) => println!("Got {} messages", msgs.len()),
Err(_) => println!("The API was too slow, skipping this tick"),
}
}
Consistency via Intervals
If you find yourself writing a loop with a sleep at the end, you might notice a slight "drift." If your API call takes 1 second and your sleep is 5 seconds, your loop actually runs every 6 seconds. If you need a steady heartbeat—say, exactly every 5 seconds regardless of how long the work takes—use tokio::time::interval. It tracks the "missed" time and tries to catch up, ensuring your cadence stays consistent over the long haul.
📋 Practical Task
Build a Resilient Heartbeat Monitor
Create a program that simulates a heartbeat monitor for a remote server. Your program should meet the following requirements:
- Use
tokio::time::intervalto trigger a "ping" every 2 seconds. - Create a mock function
async fn send_ping()that randomly takes between 100ms and 3 seconds to complete (usetokio::time::sleepandrandto simulate this). - Wrap the
send_ping()call in atokio::time::timeoutset to 1.5 seconds. - If the ping succeeds within the timeout, print "Heartbeat OK".
- If the ping times out, print "Heartbeat TIMEOUT".
- The loop should run 10 times and then exit.
Ensure that the interval continues to fire every 2 seconds even if a specific ping takes longer than 1.5 seconds to fail.
There are no comments for now.