Build your first async application with Tokio in minutes
This quickstart guide will walk you through building a simple asynchronous application with Tokio. You’ll learn the basics of async programming, task spawning, and network I/O.
Let’s start with the simplest possible Tokio program:
use tokio::io::AsyncWriteExt;use tokio::net::TcpStream;use std::error::Error;#[tokio::main]pub async fn main() -> Result<(), Box<dyn Error>> { // Open a TCP stream to the socket address. // // Note that this is the Tokio TcpStream, which is fully async. let mut stream = TcpStream::connect("127.0.0.1:6142").await?; println!("created stream"); let result = stream.write_all(b"hello world\n").await; println!("wrote to stream; success={:?}", result.is_ok()); Ok()}
This example demonstrates several key concepts:
1
The #[tokio::main] macro
The #[tokio::main] macro sets up the async runtime and allows your main function to be async. This is the entry point for all Tokio applications.
2
Async/await syntax
The await keyword pauses execution until an async operation completes. Operations like connect() and write_all() return futures that must be awaited.
3
Asynchronous I/O
TcpStream is Tokio’s non-blocking TCP socket. All I/O operations are asynchronous and won’t block other tasks.
To test this example, first start a server with ncat -l 6142 in one terminal, then run your program in another.
Now let’s build something more practical: a TCP echo server that handles multiple concurrent connections.
use tokio::io::{AsyncReadExt, AsyncWriteExt};use tokio::net::TcpListener;use std::env;use std::error::Error;const DEFAULT_ADDR: &str = "127.0.0.1:8080";const BUFFER_SIZE: usize = 4096;#[tokio::main]async fn main() -> Result<(), Box<dyn Error>> { // Allow passing an address to listen on as the first argument of this // program, but otherwise we'll just set up our TCP listener on // 127.0.0.1:8080 for connections. let addr = env::args() .nth(1) .unwrap_or_else(|| DEFAULT_ADDR.to_string()); // Next up we create a TCP listener which will listen for incoming // connections. This TCP listener is bound to the address we determined // above and must be associated with an event loop. let listener = TcpListener::bind(&addr).await?; println!("Listening on: {addr}"); loop { // Asynchronously wait for an inbound socket. let (mut socket, addr) = listener.accept().await?; // And this is where much of the magic of this server happens. We // crucially want all clients to make progress concurrently, rather than // blocking one on completion of another. To achieve this we use the // `tokio::spawn` function to execute the work in the background. // // Essentially here we're executing a new task to run concurrently, // which will allow all of our clients to be processed concurrently. tokio::spawn(async move { let mut buf = vec![0; BUFFER_SIZE]; // In a loop, read data from the socket and write the data back. loop { match socket.read(&mut buf).await { Ok(0) => { // Connection closed by peer return; } Ok(n) => { // Write the data back. If writing fails, log the error and exit. if let Err(e) = socket.write_all(&buf[0..n]).await { eprintln!("Failed to write to socket {}: {}", addr, e); return; } } Err(e) => { eprintln!("Failed to read from socket {}: {}", addr, e); return; } } } }); }}
The tokio::spawn function creates a new task that runs concurrently on the Tokio runtime. This allows the server to handle multiple clients simultaneously without blocking.
Each spawned task is lightweight (only a few hundred bytes) and can run on any thread in the runtime’s thread pool.
Tokio provides several channel types for communication between tasks. Here’s an example using the mpsc (multi-producer, single-consumer) channel:
use tokio::sync::mpsc;#[tokio::main]async fn main() { let (tx, mut rx) = mpsc::channel(32); tokio::spawn(async move { for i in 0..10 { if tx.send(i).await.is_err() { println!("receiver dropped"); return; } } }); while let Some(i) = rx.recv().await { println!("got = {}", i); }}
Channels are useful for:
Sending messages between tasks
Coordinating work across multiple async operations
Implementing producer-consumer patterns
oneshot
Single-value channel for sending one message
mpsc
Multi-producer, single-consumer channel
broadcast
Multi-producer, multi-consumer with message cloning
use tokio::time::{interval, Duration};#[tokio::main]async fn main() { let mut interval = interval(Duration::from_secs(1)); for _ in 0..5 { interval.tick().await; println!("Tick!"); }}
Sometimes you need to run CPU-intensive or blocking code. Use spawn_blocking to avoid blocking the async runtime:
use tokio::task;#[tokio::main]async fn main() { let blocking_task = task::spawn_blocking(|| { // This is running on a dedicated blocking thread pool // CPU-intensive work here expensive_computation() }); let result = blocking_task.await.unwrap(); println!("Result: {}", result);}fn expensive_computation() -> u64 { // Simulate expensive work 42}
Never run blocking operations directly in async code - it will prevent other tasks from making progress. Always use spawn_blocking for blocking work.