Skip to main content
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.

Prerequisites

Before starting, make sure you have:
  • Rust 1.71 or later installed
  • Basic familiarity with Rust syntax
  • Tokio added to your project (see Installation)

Hello, async world

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.

Building a TCP echo server

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;
                    }
                }
            }
        });
    }
}
This server showcases important Tokio patterns:

Accepting connections

let listener = TcpListener::bind(&addr).await?;

loop {
    let (mut socket, addr) = listener.accept().await?;
    // Handle socket...
}
The server binds to an address and continuously accepts incoming connections in a loop. Each accept() call waits asynchronously for a new connection.

Spawning tasks

tokio::spawn(async move {
    // Handle connection concurrently
});
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.

Reading and writing

match socket.read(&mut buf).await {
    Ok(0) => return,  // Connection closed
    Ok(n) => {
        socket.write_all(&buf[0..n]).await?;
    }
    Err(e) => {
        eprintln!("Error: {}", e);
        return;
    }
}
AsyncReadExt and AsyncWriteExt provide async methods for reading from and writing to I/O streams.

Testing the server

1

Run the server

cargo run
You should see:
Listening on: 127.0.0.1:8080
2

Connect with telnet

In another terminal:
telnet localhost 8080
3

Send messages

Type any message and press Enter. The server will echo it back to you.

Working with channels

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

Timeouts and intervals

Tokio’s time module provides utilities for working with time:

Timeouts

use tokio::time::{timeout, Duration};

#[tokio::main]
async fn main() {
    let result = timeout(Duration::from_secs(5), async {
        // Long-running operation
        tokio::time::sleep(Duration::from_secs(10)).await;
        "completed"
    }).await;

    match result {
        Ok(v) => println!("Success: {}", v),
        Err(_) => println!("Operation timed out"),
    }
}

Intervals

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!");
    }
}

Common patterns

Handling blocking operations

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.

Graceful shutdown

Use tokio::select! to handle cancellation and graceful shutdown:
use tokio::signal;

#[tokio::main]
async fn main() {
    tokio::select! {
        _ = signal::ctrl_c() => {
            println!("Shutting down...");
        }
        _ = run_server() => {
            println!("Server completed");
        }
    }
}

async fn run_server() {
    // Server logic
}

Next steps

You now have the fundamentals to build async applications with Tokio. Here are some resources to continue learning:

API documentation

Complete API reference with detailed examples

Tokio tutorial

In-depth guide covering advanced topics

Examples

Real-world examples from the Tokio repository

Discord community

Get help and connect with other Tokio users

Key takeaways

  • Use #[tokio::main] to set up the async runtime
  • Spawn tasks with tokio::spawn for concurrent execution
  • Use async I/O operations instead of blocking ones
  • Leverage channels for task communication
  • Use spawn_blocking for CPU-intensive work
  • Handle graceful shutdown with tokio::select!
Start simple and add complexity as needed. The full feature flag gives you access to all of Tokio’s capabilities while you’re learning.

Build docs developers (and LLMs) love