Skip to main content
Lesson 2 of 3Beginner⏱ 10 mins

Async/Await and Tokio

Understanding non-blocking I/O in Actix.

33% of path

Learning Objectives

  • Understand Futures
  • Learn how Tokio powers Actix

##Doing many things at once with async

A web server spends most of its time waiting: for the network, for a database, for a file. Blocking a whole thread during that wait would waste resources. Asynchronous Rust lets a few threads juggle thousands of waiting requests.

#async fn and await

An async fn does not run when you call it. It returns a future, which is a description of work that will complete later. You run it by awaiting it:

code_blocksrust
async fn fetch_name() -> String {
    "Ada".to_string()
}

async fn greet() {
    let name = fetch_name().await;   // pause here, let others run
    println!("Hello, {name}");
}

.await means "pause this task until the result is ready, and let the runtime run something else meanwhile". Futures in Rust are lazy: if you never await one, it never runs.

#The runtime

Futures need a runtime to drive them. Actix Web uses Tokio. The attribute #[actix_web::main] starts that runtime for you, which is why async fn main works in your server.

code_blocksrust
#[actix_web::main]
async fn main() {
    greet().await;
}

#Why handlers are async

Every Actix handler is an async fn. While one request awaits a database query, the same worker thread serves other requests. That is how a handful of threads handle huge traffic.

#Running things concurrently

Awaiting two futures one after the other takes the sum of their times. Use tokio::join! to run them together:

code_blocksrust
use tokio::time::{sleep, Duration};

async fn task(name: &str, ms: u64) {
    sleep(Duration::from_millis(ms)).await;
    println!("{name} done");
}

#[actix_web::main]
async fn main() {
    tokio::join!(task("a", 300), task("b", 300));
    // total: about 300ms, not 600ms
}

#Never block the runtime

Calling a blocking function (heavy computation, std::thread::sleep, synchronous file or network calls) inside an async handler freezes that worker so it cannot serve other requests. Use async equivalents such as tokio::time::sleep, or move blocking work to a thread pool with web::block:

code_blocksrust
let result = web::block(move || expensive_hash(&password)).await?;

#Rule of thumb

If it waits on I/O, use async. If it burns CPU for a long time, send it to web::block.

"What If?" Experiment

Learning by doing. Modify the code to see what happens.

Goal

Compare sequential and concurrent awaiting.

Starting Code
work(200).await; work(200).await;
Suggested Modification

Replace the two awaits with tokio::join!(work(200), work(200)).

Expected Result

The elapsed time drops from about 400ms to about 200ms.

Explanation: Sequential awaits wait for each other; join! lets both wait at the same time.

Common Mistakes

Anticipate and avoid these frequent pitfalls when working with this concept.

!

Blocking inside a handler

std::thread::sleep blocks the whole worker thread, not just this request.

Incorrect Pattern
std::thread::sleep(std::time::Duration::from_secs(2));
Correct Pattern
tokio::time::sleep(std::time::Duration::from_secs(2)).await;

Explanation: The async sleep yields to the runtime, so other requests keep being served while this one waits.

!

Calling an async function without await

The future is created but never polled, so nothing happens.

Incorrect Pattern
save_user(user);
Correct Pattern
save_user(user).await;

Explanation: Rust warns about unused futures. Always await a future or hand it to the runtime.

Practical Exercise

Run two tasks concurrently

Task

Use tokio::join! so that two simulated 200ms tasks finish in about 200ms total.

Requirements

  • Write an async fn that sleeps for a given number of milliseconds
  • Run two of them with tokio::join!
  • Print the total elapsed time

Need a hint?

Try solving the exercise in your local environment first. Once you have a solution, or if you get completely stuck, reveal the answer.

Real-World Application

A typical endpoint loads a user and their orders from the database. Running both queries with join! halves the latency, and async I/O lets one server handle tens of thousands of simultaneous connections.

Key Takeaways

  • An async fn returns a lazy future that only runs when awaited.
  • #[actix_web::main] starts the Tokio-based runtime.
  • tokio::join! runs independent futures concurrently.
  • Never block inside async code; use async APIs or web::block.

Finished reading?

Mark this lesson as done to track your progress and earn XP.