Async Rust with Tokio: A Beginner-Friendly Guide

A
Author
05 Jul, 2026 1 min read 1.04K views

Async Rust lets one thread handle thousands of network connections. Rust provides async/await syntax; a runtime such as Tokio actually runs the futures.

Setup

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

Your first async program

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let body = reqwest::get("https://httpbin.org/ip").await?.text().await?;
    println!("{body}");
    Ok(())
}

Futures are lazy: nothing happens until you .await them (or spawn them).

Run things concurrently

let (users, orders) = tokio::join!(fetch_users(), fetch_orders()); // both at once

// Background tasks
let handle = tokio::spawn(async move {
    process_queue().await
});
let result = handle.await?;   // JoinHandle

Timeouts

use std::time::Duration;
use tokio::time::timeout;

match timeout(Duration::from_secs(5), fetch_users()).await {
    Ok(users) => println!("{} users", users.len()),
    Err(_) => eprintln!("timed out"),
}

Channels between tasks

use tokio::sync::mpsc;

let (tx, mut rx) = mpsc::channel::<String>(100);

tokio::spawn(async move {
    for i in 0..3 {
        tx.send(format!("job {i}")).await.unwrap();
    }
});

while let Some(job) = rx.recv().await {
    println!("processing {job}");
}

Common pitfalls

  • Blocking in async code (heavy CPU work, std::thread::sleep, blocking file I/O) stalls other tasks. Use tokio::task::spawn_blocking or async equivalents.
  • Holding a std::sync::Mutex guard across an .await; use tokio::sync::Mutex or release it first.
  • tokio::spawn needs 'static futures, which is why async move and Arc appear so often.
Was this article helpful?

Comments

No comments yet. Have a question or a tip? Be the first to share it.

Leave a comment

Comments are reviewed before they appear.