When I first started with Rust, I thought concurrency was just about threads. But in the world of high-performance network services, OS threads are too heavy. That’s where the tokio runtime comes in. In this rust concurrency tutorial tokio guide, I’m going to walk you through how to move from synchronous code to a fully asynchronous architecture that can scale to millions of requests.
Rust’s approach to concurrency is unique because it leverages the ownership model to prevent data races at compile time. If you’ve already read my rust memory management deep dive, you know how the borrow checker works; async Rust just adds a layer of complexity called Futures.
Prerequisites
Before we dive into the code, ensure you have the following set up in your environment:
- Rust toolchain (stable) installed via rustup.
- Basic familiarity with
ResultandOptiontypes. - A working knowledge of
Cargofor dependency management.
Step 1: Setting Up Your Tokio Project
First, create a new binary project and add tokio to your Cargo.toml. I always recommend enabling the full feature set to avoid hunting down missing macros like #[tokio::main].
# Create the project
cargo new tokio-demo
cd tokio-demo
# Add tokio with full features
cargo add tokio --features full
Now, look at your main.rs. A standard Rust main function cannot be async. We use the #[tokio::main] macro to wrap our asynchronous entry point in a runtime.
#[tokio::main]
async fn main() {
println!("Hello from the async world!");
}
Step 2: Spawning Concurrent Tasks
The real power of Tokio lies in tokio::spawn. This allows you to create a new ‘green thread’ (task) that the runtime can schedule across available CPU cores. Unlike OS threads, these are incredibly lightweight.
In my experience, the biggest mistake beginners make is calling .await on every single future sequentially. That’s not concurrency; that’s just a slow synchronous program. To actually run things in parallel, you must spawn them.
use tokio::time::{sleep, Duration};
async fn fetch_data(id: u32) {
println!("Task {}: Starting fetch...", id);
sleep(Duration::from_secs(2)).await;
println!("Task {}: Data received!", id);
}
#[tokio::main]
async fn main() {
let mut handles = vec![];
for i in 1..=3 {
// We spawn a task and get a JoinHandle back
let handle = tokio::spawn(async move {
fetch_data(i).await;
});
handles.push(handle);
}
// Wait for all tasks to complete
for handle in handles {
handle.await.unwrap();
}
}
As shown in the terminal output logic above, these three tasks will run concurrently, and the total execution time will be roughly 2 seconds, not 6.
Step 3: Communicating with Channels
Sharing state between tasks usually involves Arc<Mutex<T>>, but as the saying goes: “Do not communicate by sharing memory; instead, share memory by communicating.”
Tokio provides several channel types. I typically use mpsc (multi-producer, single-consumer) for coordinating worker tasks. Here is how I implement a simple producer-consumer pattern:
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(32);
tokio::spawn(async move {
for i in 1..=5 {
tx.send(format!("Message {}", i)).await.unwrap();
}
});
while let Some(message) = rx.recv().await {
println!("Received: {}", message);
}
}
Pro Tips for Tokio Performance
- Never block the runtime: Never use
std::thread::sleepor long-running synchronous CPU loops inside anasync fn. Usetokio::time::sleeportokio::task::spawn_blockingfor heavy computations. - Prefer
select!for timeouts: Use thetokio::select!macro to wait on multiple async operations and respond to whichever finishes first. - Limit Concurrency: Don’t spawn 100,000 tasks that all hit a database. Use a
Semaphoreto limit active permits.
Troubleshooting Common Async Errors
If you see the error future cannot be sent between threads safely, it’s usually because you’re trying to hold a non-Send type (like a RefCell or a raw pointer) across an .await point. Switch to tokio::sync::Mutex instead of the standard library Mutex to avoid deadlocking the executor.
What’s Next?
Now that you have the basics of the rust concurrency tutorial tokio down, it’s time to apply this to a real project. I highly recommend building a web server. If you’re deciding on a framework, check out my comparison of axum vs actix-web 2026 to see which one integrates better with your current workflow.
Want to push your Rust skills further? Explore advanced patterns like the Actor model or dive into low-level networking with mio.