Skip to content

Async Introduction

Rust’s async/.await syntax lets you write non-blocking code that looks like ordinary sequential code. Under the hood, an async fn returns a Future — a value that represents a computation that may not have completed yet.

use tokio::time::{sleep, Duration};
async fn fetch_data(id: u32) -> String {
sleep(Duration::from_millis(10)).await;
format!("data for id={}", id)
}
#[tokio::main]
async fn main() {
let result = fetch_data(1).await;
println!("{}", result);
}

The .await keyword suspends the current async function until the Future it is applied to resolves. While suspended, the executor (tokio) can run other tasks.

A Future<Output = T> is a trait that represents a value of type T that will be available at some point in the future. Calling an async fn does not execute the body immediately — it returns a Future. The body only runs when you .await it (or poll it via an executor).

// This does nothing yet — just creates a Future
let fut = fetch_data(42);
// This runs the body and blocks until complete
let result = fut.await;

This lazy evaluation is why Rust async is zero-cost: futures that are never awaited never run.

Running Tasks Concurrently with tokio::join!

Section titled “Running Tasks Concurrently with tokio::join!”

To run multiple async tasks at the same time, use tokio::join!. It drives all futures concurrently on the same thread (or across worker threads, depending on the runtime configuration).

use tokio::time::{sleep, Duration};
async fn task(name: &str, ms: u64) -> String {
sleep(Duration::from_millis(ms)).await;
format!("{} done", name)
}
#[tokio::main]
async fn main() {
let (a, b, c) = tokio::join!(
task("alpha", 30),
task("beta", 20),
task("gamma", 10),
);
println!("{}", a);
println!("{}", b);
println!("{}", c);
}

All three tasks start immediately. tokio::join! waits until all three complete and returns their results in order.

tokio::spawn launches a task onto the tokio thread pool. It returns a JoinHandle similar to std::thread::spawn.

use tokio::task;
#[tokio::main]
async fn main() {
let handle = task::spawn(async {
// runs concurrently on the tokio thread pool
42_u32
});
let result = handle.await.unwrap();
println!("Spawned task returned: {}", result);
}
CriterionOS Threads (std::thread)Async (tokio)
CPU-bound workPreferredUse spawn_blocking
I/O-bound workWasteful (blocked thread)Preferred
Number of concurrent tasksHundreds (OS limit)Millions (lightweight)
Stack size~8 MB per thread~few KB per task
External crate neededNoYes (tokio)
Terminal window
cargo add tokio --features full

Or in Cargo.toml:

[dependencies]
tokio = { version = "1", features = ["full"] }

Then annotate your entry point:

#[tokio::main]
async fn main() {
// your async code here
}

#[tokio::main] is a procedural macro that wraps your async fn main in a call to tokio::runtime::Runtime::block_on, starting the tokio event loop.

What does an async fn return in Rust?
Why does Rust require an external executor like tokio to run async code?
What is the difference between tokio::join! and sequential awaits?
When should you use spawn_blocking instead of a regular tokio::spawn?