Skip to content

Rust Async/Await — A Practical Guide

DodaTech Updated 2026-06-21 7 min read

Rust async/await provides zero-cost asynchronous programming where futures are polled lazily, enabling thousands of concurrent connections per thread with no runtime overhead for unused operations.

What You'll Learn

In this tutorial, you'll learn how Rust async/await works, how futures are polled, how the Tokio runtime schedules tasks, how to write async functions, and how to build high-performance async network services.

Why It Matters

Traditional threading hits limits at scale — each thread consumes megabytes of stack memory. Async Rust enables handling tens of thousands of concurrent connections on a single thread. This is essential for modern network services: web servers, proxies, databases, and real-time communication systems.

Real-World Use

Tokio powers Discord, AWS Lambda runtime, and Cloudflare's edge network. The Hyper HTTP library uses async for high-throughput request handling. SQLx uses async for database drivers. Doda Browser's networking layer uses async for parallel downloads without thread overhead.

flowchart LR
    subgraph "Single OS Thread"
        T1[Task 1: waiting for I/O] -->|yield| Runtime
        T2[Task 2: ready to run] -->|poll| Runtime
        T3[Task 3: waiting for timer] -->|yield| Runtime
        Runtime -->|wake| T1
        Runtime -->|wake| T3
    end
    note: "Many tasks, one thread -- cooperative multitasking"
â„šī¸ Info

Prerequisites: Rust Concurrency, Closures, and Smart Pointers.

Async Functions and Futures

An async function returns a Future. It does nothing until polled.

use std::time::Duration;

async fn do_work(id: u32) -> String {
    // Simulates async I/O without blocking
    tokio::time::sleep(Duration::from_millis(100)).await;
    format!("Task {} completed", id)
}

#[tokio::main]
async fn main() {
    let future = do_work(42); // creates Future, does NOT execute yet
    println!("Future created, not yet executed");
    let result = future.await; // executes the future
    println!("{}", result);
}

Expected output:

Future created, not yet executed
Task 42 completed

Running Multiple Tasks Concurrently

Join multiple futures to run them concurrently.

use std::time::Duration;

async fn fetch_data(id: u32) -> String {
    tokio::time::sleep(Duration::from_millis(100 * id as u64)).await;
    format!("Data from source {}", id)
}

#[tokio::main]
async fn main() {
    let start = std::time::Instant::now();

    // Run three fetches concurrently
    let results = tokio::join!(
        fetch_data(1),
        fetch_data(2),
        fetch_data(3),
    );

    let elapsed = start.elapsed();
    println!("Results: {:?}", results);
    println!("Elapsed: {:?} (not sequential sum)", elapsed);
}

Expected output:

Results: ("Data from source 1", "Data from source 2", "Data from source 3")
Elapsed: 303ms (not sequential sum)

Spawning Tokio Tasks

tokio::spawn creates independent tasks that run concurrently on the Tokio runtime.

use std::time::Duration;

async fn scan_file(path: &str) -> String {
    tokio::time::sleep(Duration::from_millis(100)).await;
    format!("Scanned: {}", path)
}

#[tokio::main]
async fn main() {
    let mut handles = vec![];

    for i in 0..5 {
        let handle = tokio::spawn(async move {
            scan_file(&format!("file_{}.txt", i)).await
        });
        handles.push(handle);
    }

    for handle in handles {
        let result = handle.await.unwrap();
        println!("{}", result);
    }
}

Expected output:

Scanned: file_0.txt
Scanned: file_1.txt
Scanned: file_2.txt
Scanned: file_3.txt
Scanned: file_4.txt

Async I/O

Tokio provides async versions of standard I/O operations.

use tokio::fs::File;
use tokio::io::AsyncReadExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = "Cargo.toml";
    let mut file = File::open(path).await?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).await?;

    let lines: Vec<&str> = contents.lines().collect();
    println!("File {} has {} lines", path, lines.len());
    println!("First line: {}", lines.first().unwrap_or(&""));

    // Async networking
    let mut stream = tokio::net::TcpStream::connect("127.0.0.1:8080").await;
    match stream {
        Ok(_) => println!("Connected to server"),
        Err(e) => println!("Connection failed (expected): {}", e),
    }
    Ok(())
}

Expected output:

File Cargo.toml has [number] lines
First line: [package]
Connection failed (expected): Connection refused

Building an Async Server

A simple async TCP echo server demonstrates the power of async.

use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpListener;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("127.0.0.1:0").await?;
    let port = listener.local_addr()?.port();
    println!("Server listening on port {}", port);

    // Connect to self for demonstration
    let client = tokio::spawn(async move {
        let mut stream = tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port)).await.unwrap();
        stream.write_all(b"hello server\n").await.unwrap();
        let mut buf = vec![0u8; 1024];
        let n = stream.read(&mut buf).await.unwrap();
        println!("Client received: {}", String::from_utf8_lossy(&buf[..n]));
    });

    let (mut stream, addr) = listener.accept().await?;
    println!("Accepted connection from {}", addr);
    let mut reader = BufReader::new(&mut stream);
    let mut line = String::new();
    reader.read_line(&mut line).await?;
    stream.write_all(format!("Echo: {}", line).as_bytes()).await?;

    client.await?;
    Ok(())
}

Async and Security

Async Rust's cooperative multitasking model has security implications:

  • No preemption: A misbehaving async task can block the entire thread if it never yields
  • Cancellation safety: Futures can be dropped at await points — code must handle this safely
  • Deadlock freedom: Holding a std::sync::Mutex across await points causes deadlocks; use tokio::sync::Mutex instead
  • Resource limits: Spawning unlimited tasks can exhaust memory without backpressure

Durga Antivirus Pro's async scan pipeline uses bounded channels and cancellation tokens to ensure backpressure and graceful shutdown.

Common Mistakes

1. Blocking the Async Runtime

Calling std::thread::sleep or synchronous I/O inside async code blocks the entire runtime thread. Use tokio::time::sleep instead.

2. Holding Std Mutex Across .await

std::sync::MutexGuard is not Send across .await points. Use tokio::sync::Mutex for async code.

3. Not Spawning Tasks for Parallelism

Sequentially awaiting multiple async functions runs them one at a time. Use tokio::join! or tokio::spawn for concurrency.

4. Ignoring Cancellation Safety

Futures can be dropped at any .await. Code after .await may not run. Design state machines that handle partial execution.

5. Using Unbounded Channels Without Backpressure

tokio::sync::mpsc::unbounded_channel grows without limit. Use bounded channels and handle send errors for backpressure.

Practice Questions

1. What is a Future in Rust? A Future is a value that may not be ready yet. It is polled by the async runtime. When it yields Poll::Pending, the runtime schedules other tasks. When ready, it returns Poll::Ready(value).

2. How does tokio::spawn differ from direct .await? tokio::spawn creates a new independent task that runs concurrently. Direct .await blocks the current task until the future completes. Use spawn for parallelism, await for sequential composition.

3. What is the Tokio runtime? Tokio is an async runtime that provides I/O drivers, timers, and a task scheduler. It manages a thread pool and polls futures, waking them when I/O events occur.

4. Why should you not use std::sync::Mutex in async code? std::sync::Mutex blocks the OS thread when locked. In async code, this blocks all tasks on that thread. Use tokio::sync::Mutex which yields the task instead of blocking.

5. Challenge: Build an async web crawler that fetches multiple URLs concurrently using reqwest, with a limit of 10 concurrent requests.

Mini Project: Async File Scanner

use tokio::fs;
use tokio::io::AsyncReadExt;
use std::time::Instant;
use tokio::sync::Semaphore;

async fn scan_file(path: &str, pattern: &str) -> Result<(String, usize), Box<dyn std::error::Error>> {
    let mut file = fs::File::open(path).await?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).await?;
    let matches = contents.matches(pattern).count();
    Ok((path.to_string(), matches))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let start = Instant::now();
    let files = vec!["Cargo.toml", "src/main.rs"];
    let semaphore = Semaphore::new(2); // max 2 concurrent scans

    let mut handles = vec![];
    for file in &files {
        let permit = semaphore.acquire().await.unwrap();
        let f = file.to_string();
        handles.push(tokio::spawn(async move {
            let result = scan_file(&f, "rust").await;
            drop(permit);
            result
        }));
    }

    for handle in handles {
        match handle.await.unwrap() {
            Ok((path, count)) => println!("{}: {} matches", path, count),
            Err(e) => println!("Error: {}", e),
        }
    }

    println!("Total time: {:?}", start.elapsed());
    Ok(())
}

FAQ

What is the difference between threads and async tasks?

Threads are OS-level and each has its own stack (MBs). Async tasks are user-space and share a thread stack (bytes each). Async is better for I/O-bound workloads; threads are better for CPU-bound work.

Is Rust async zero-cost?

Yes. Rust's async/await compiles to a state machine that only allocates memory when spawned as a task. There is no built-in runtime cost — you pay only for what you use.

Should I use async for CPU-bound work?

No. Async is for I/O-bound work where tasks spend most time waiting. For CPU-bound work, use threads or rayon. Async on a single thread cannot utilize multiple CPU cores.

Rust Concurrency
Unsafe Rust
Performance Optimization

What's Next

Explore Unsafe Rust for low-level control, and Performance Optimization for profiling and tuning async code.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro