Async Rust compiles to state machines driven by an executor (usually Tokio for network services). Futures must be Send to run across thread pools safely.
Core concepts
- async fn: Returns an impl Future — does nothing until
.awaited on a runtime. — Async Rust: async/await - Tokio:
#[tokio::main]macro;tokio::spawnfor concurrent tasks. — Tokio docs - await points: Yield control without blocking OS threads — good for HTTP/DB I/O. — Async Rust book
- Channels:
tokio::sync::mpscfor message passing between tasks. — Tokio mpsc - Don't block the runtime: Offload CPU work with
spawn_blocking. — Tokio: CPU-bound tasks
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let task = tokio::spawn(async {
sleep(Duration::from_millis(100)).await;
42
});
println!("result = {}", task.await.unwrap());
}Concept videos
YouTube lessons for these topics. On a large screen they open in the reading panel next to the roadmap, same as the article links on each core concept.
- Async Rust — Rust in 100 Seconds (Fireship)
Explanation
Async Rust is cooperative multitasking: futures poll until they need I/O, then yield. Tokio is the runtime that schedules those polls across thread pools.
async fn
Calling async fn does not run body immediately — it returns a Future, a recipe waiting for an executor. .await inside resumes after subtasks complete. Without a runtime, futures are inert.
Tokio
#[tokio::main] boots a multithreaded runtime for binaries. tokio::spawn runs independent async tasks concurrently — like lightweight threads managed by the executor.
await points
Each .await is a yield point where other tasks progress. Long CPU loops without await starve the runtime — the UI or server stops responding.
Channels
Channels pass messages between tasks safely without shared mutable state. Producers send; consumers receive — backpressure and timeouts become explicit design choices.
Don't block the runtime
Blocking syscalls (std::thread::sleep, heavy compute) inside async tasks clog worker threads. spawn_blocking moves blocking work to a dedicated pool so async I/O stays smooth.
What Is It?
async fn returns a Future polled by an executor until Ready.
Tokio provides tasks, timers, and async IO for TCP/HTTP.
Send/Sync bounds govern which futures can run on thread pools.
Why Should I Care?
Axum and hyper services are async-first — you need await discipline.
Blocking std IO inside async tasks stalls throughput.
Real World Analogy
A Future in Rust is a microwave dinner with a timer sticker — doing nothing until you .await it in an executor kitchen that rotates many meals.
Visual Mental Model
async fn -> Future
executor (Tokio) polls
.await yield pointsBeginner Explanation
Async Rust compiles to state machines. Tokio is the common runtime for network services. Futures must be Send to move across threads safely.
Smallest Possible Example
# async fn hello() {
println!("async ready");
}
#[tokio::main]
async fn main() {
hello().await;
}Common Mistakes
❌ Calling std::thread::sleep in async code.
Blocks executor threads — use tokio::time::sleep.
❌ Spawning tasks without JoinHandle error handling.
Panics in tasks can fail silently unless observed.
❌ Ignoring Pin when writing low-level futures.
Self-referential futures need pinning — libraries hide this until advanced work.
Real Project Example
Cloudflare Workers Rust SDK and many Axum microservices run on Tokio.
Discord migrated hot paths to Rust async for scale.
Build This Mini Project
Create a Tokio project with two concurrent tasks using tokio::join! and log completion order.
Interview Question
What does .await do?
It suspends the async function until the future completes, yielding control so the executor can poll other tasks — cooperative concurrency without blocking threads.
Key Takeaways
- Futures are lazy until awaited.
- Pick an executor (usually Tokio).
- Do not block async worker threads.
- Send/Sync matter for multithreaded runtimes.
Resources
The 60-second story
You pin a note on a board: “call the supplier.” Nothing happens because a note is not a person walking. async fn returns a future, which is that note. It describes work. .await is someone actually walking the note on a runtime. Until then, the supplier has not been dialed. Beginners await-forget and think Rust is ignoring them. The note was never picked up.
Tokio is a common crew that agrees to walk those notes. #[tokio::main] starts the crew in main. tokio::spawn hands a note to another walker so two calls can be in flight. Await points are where a walker puts the phone on speaker and goes to pin another note, instead of blocking a whole worker thread on hold music.
Channels are the little mail slots between walkers. tokio::sync::mpsc is many senders, one receiver, a line of messages. You use that when tasks should talk without sharing a notebook and a pen.
Blocking the runtime is bringing a cement mixer onto the walkway. A CPU-heavy loop or a sync file read that never awaits will stall the crew that was supposed to be walking notes. spawn_blocking puts the mixer in a side lot.
Map the board.
- A future is a note, not the walk.
.awaitpicks the note up.- Tokio is the crew.
spawnis another walker.- An await point is “phone on speaker, keep moving.”
- A channel is a mail slot between walkers.
spawn_blockingis the side lot for heavy tools.
async fn ping() -> &'static str {
"pong"
}Calling ping() only builds the note. A runtime has to await it. That two-step is the whole beginner trap, and it is also the feature: you can pass notes around, combine them, and decide who walks them.
Async Rust looks heavier than JavaScript because the note’s type has to be known and the crew is a library, not a hidden manager inside the browser. The payoff is the same plot: do not freeze a worker on I/O. The ownership rules still apply. You cannot hand the same cash box to two walkers and hope.
Mental model: notes on a board versus people walking. If you did not await, you did not do the work. If the work is a cement mixer, do not park it in the hallway.
Explain it in a sentence: async functions are lazy, Tokio runs them, and blocking calls need a side door.

