---
title: "23.3 Using Tokio"
description: "Adding the Tokio runtime, the #[tokio::main] attribute, spawning tasks, async sleep versus blocking sleep, and a small concurrent example."
url: "https://learnrust.net/chapter-23/using-tokio/"
last_updated: "2026-06-13"
---

# 23.3 Using Tokio

Async code needs a runtime to run it (lesson [23.2](https://learnrust.net/chapter-23/async-await-and-futures/)), and Rust ships none. The most widely used one, the de facto standard for network services and async applications, is **Tokio**. This lesson adds it, gets async code actually executing, and shows the thing async is *for*: doing many waits concurrently. It's your third dependency, after `rand` and `rayon`, and like those, one line in `Cargo.toml` unlocks a large amount of capability.

## Adding Tokio

Add it with `cargo add`, requesting all its features for now:

```bash
$ cargo add tokio --features full
```

```
      Adding tokio v1.40.0 to dependencies
```

That writes a line like `tokio = { version = "1", features = ["full"] }` into `Cargo.toml`. (The exact patch version varies; these examples target the `1.x` series.) The `full` feature set turns on everything, the timer, the task scheduler, async file and network IO. Real projects trim this down to just the features they use, but `full` is the right choice while learning.

## #[tokio::main] starts the runtime

Recall the wall from last lesson: `main` can't be `async`, so it can't `.await` anything, so nothing drives your futures. Tokio's answer is an **attribute** that rewrites your `main` for you. Write an `async fn main` and tag it `#[tokio::main]`:

```rust
#[tokio::main]
async fn main() {
    println!("hello from async main");
    say_hi().await;
}

async fn say_hi() {
    println!("hi from an async function");
}
```

```
hello from async main
hi from an async function
```

It runs. The `#[tokio::main]` attribute is doing something specific and worth understanding: it transforms your `async fn main` into an ordinary `fn main` that starts a Tokio runtime and uses it to drive your async body to completion. In other words, it generates the non-async "block on this future" wrapper that the last lesson said had to exist somewhere. Now your `main` *is* async, so `.await` works inside it, and the async chain finally has a place to bottom out. (Attributes like this are produced by procedural macros, which chapter 24 covers; for now, read `#[tokio::main]` as "set up the runtime and run my async main.")

## Tasks: tokio::spawn

Inside a runtime, you create concurrent tasks with `tokio::spawn`, the async cousin of `thread::spawn` (chapter 22). It takes a future and runs it as an independent task, returning a handle you can `.await` for its result:

```rust
#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        println!("task running");
        10 + 5
    });

    let result = handle.await.unwrap();
    println!("task returned {result}");
}
```

```
task running
task returned 15
```

`tokio::spawn(async { ... })` takes an **async block** (a future written inline, the `async` equivalent of a closure body) and schedules it as a task. The runtime runs it concurrently with the rest of `main`; `handle.await` waits for it and yields its value (it's a `Result`, since the task could panic, so we `unwrap`). Spawning many tasks is how a server handles many connections: one lightweight task each, the runtime juggling them all.

## Async sleep, and the cardinal rule

Here's where async earns its keep, and where the first footgun lives. To wait without blocking, you use the runtime's *async* sleep, `tokio::time::sleep`, awaited, **not** the `std::thread::sleep` from chapter 22:

```rust
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let a = tokio::spawn(async {
        sleep(Duration::from_millis(100)).await;
        println!("task A done");
    });
    let b = tokio::spawn(async {
        sleep(Duration::from_millis(100)).await;
        println!("task B done");
    });

    a.await.unwrap();
    b.await.unwrap();
    println!("both done");
}
```

```
task A done
task B done
both done
```

Both tasks sleep 100 milliseconds, yet the whole program finishes in *about* 100 milliseconds, not 200. That's the payoff: when task A hits `sleep(...).await`, it doesn't block the thread, it steps aside, the runtime runs task B, which also yields at its sleep, and both waits happen *concurrently* on the same thread. Two 100ms waits overlap into one. Scale that to ten thousand network reads and you see why async runs servers.

The crucial detail is `sleep(...).await`, the async sleep. If you had used `std::thread::sleep` inside a task instead, it would have **blocked the whole thread**, freezing every other task scheduled on it, and the two sleeps would have run back to back for 200ms total. That mistake, calling a blocking operation inside async, is the number-one async bug, and the next lesson dissects it. For now, the rule: inside async code, use the runtime's async versions of waiting operations (`tokio::time::sleep`, Tokio's async file and network IO), never the blocking standard-library ones.

> **Async and blocking operations don't mix**
>
> The defining mistake in async Rust is calling a *blocking* operation, `std::thread::sleep`, a synchronous file read, a CPU-heavy loop, inside an async task. Async works by tasks voluntarily yielding the thread at `.await` points; a blocking call never yields, so it freezes the thread and stalls *every other task* the runtime had scheduled there. One blocking call can bring a whole server to a crawl. The fix is to use the async equivalent (and await it), or, for unavoidable blocking/CPU work, hand it to a dedicated facility (`tokio::task::spawn_blocking`). Lesson [23.4](https://learnrust.net/chapter-23/async-pitfalls/) treats this in full; internalize the rule now.

## A small concurrent fetcher, in shape

Real async code mostly waits on the network. We can't make live network calls here, but the *shape* of a concurrent fetcher is exactly the sleep example with the sleeps standing in for requests: spawn a task per URL, each awaiting its (slow, network-bound) fetch, then await all the handles. Because the requests are IO-bound waits, they overlap, ten fetches that each take 200ms finish in about 200ms total, not two seconds. That structure, spawn-many-then-await-all, over genuinely network-bound work, is what async was built for, and it's the engine inside the web server you'll build in chapter 26 (built there on threads, to keep that capstone runtime-free, but the async version is a natural variation).

## Quiz time

**Question #1**

What does the `#[tokio::main]` attribute do?

<details class="solution">
<summary>Show solution</summary>

It transforms your `async fn main` into an ordinary `fn main` that starts a Tokio runtime and drives the async body to completion. It supplies the non-async "run this future" wrapper that async code requires (since `main` can't normally be async or `.await`). With it, your `main` is effectively async, so `.await` works inside it, and the async call chain has somewhere to bottom out.

</details>

**Question #2**

In the two-tasks-each-sleeping-100ms example, why does the program finish in about 100ms rather than 200ms?

<details class="solution">
<summary>Show solution</summary>

Because the sleeps are *async* (`tokio::time::sleep(...).await`): when a task awaits the sleep, it yields the thread instead of blocking it, so the runtime runs the other task, which also yields at its sleep. The two 100ms waits happen concurrently (overlapping) on the same thread rather than one after the other. That non-blocking overlap of waits is exactly what async provides.

</details>

**Question #3**

Why must you use `tokio::time::sleep` instead of `std::thread::sleep` inside an async task?

<details class="solution">
<summary>Show solution</summary>

`std::thread::sleep` *blocks the thread*: it doesn't yield, so it freezes every other task the runtime scheduled on that thread, destroying the concurrency async is supposed to provide. `tokio::time::sleep(...).await` is non-blocking, it yields the thread while waiting, letting other tasks run. The general rule: inside async code, use the runtime's async versions of waiting operations and `.await` them, never the blocking standard-library equivalents.

</details>

Tokio makes async code run, and you've seen both its payoff (overlapping waits) and its sharpest edge (blocking calls). The next lesson ([23.4](https://learnrust.net/chapter-23/async-pitfalls/)) is the honest field guide promised since the chapter opened: the specific ways async bites, blocking the executor, `Send` troubles, cancellation, and how to step around each.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
