Skip to main content
โšก Calmops

High-Performance Networking: Tokio Runtime Explained

Introduction

Tokio is Rust’s de facto standard async runtime, enabling millions of concurrent tasks on a single machine. Understanding Tokio is essential for building high-performance network services.

This guide covers Tokio architecture, async patterns, and real-world networking examples.


Why Async?

Concurrency Models Comparison

Threads vs Async in Rust:

Model           Memory/conn  Context Switches  Latency  Max Connections
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Thread pool     ~2MB         1000s/sec         high     10k-100k
Async (Tokio)   ~50bytes     100-1000/sec      low      1M+

Example: 10,000 concurrent connections
- Thread pool:   20GB RAM
- Tokio:         500MB RAM (40x savings)

When to Use Async

USE ASYNC:
โœ… Network I/O (HTTP, databases, WebSockets)
โœ… 1000s of concurrent connections
โœ… Want low memory footprint
โœ… Latency-sensitive applications

USE THREADS:
โœ… CPU-bound work (calculations, compression)
โœ… Few concurrent tasks
โœ… Need true parallelism on multi-core

Tokio Runtime Overview

What is Tokio?

Tokio = Async Runtime
โ”œโ”€โ”€ Work-stealing scheduler
โ”œโ”€โ”€ I/O multiplexing (epoll/kqueue)
โ”œโ”€โ”€ Timer management
โ”œโ”€โ”€ Networking libraries
โ””โ”€โ”€ Synchronization primitives

Architecture Diagram

                    Tokio Runtime
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ”‚        Scheduler (thread pool)         โ”‚
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”‚
    โ”‚  โ”‚  W1  โ”‚  W2  โ”‚  W3  โ”‚  W4  โ”‚        โ”‚
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ”‚
    โ”‚        (work stealing)                 โ”‚
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    โ”‚     I/O Reactor (epoll/kqueue)        โ”‚
    โ”‚  Events from socket/file descriptors  โ”‚
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    โ”‚   Timer Wheel + Timers                โ”‚
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Getting Started with Tokio

Minimal Setup

[dependencies]
tokio = { version = "1.35", features = ["full"] }

Basic Async Function

use tokio::time::sleep;
use std::time::Duration;

#[tokio::main]
async fn main() {
    println!("Start");
    
    sleep(Duration::from_secs(1)).await;
    
    println!("After 1 second");
}

The #[tokio::main] Macro

The macro expands to:

fn main() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async {
        println!("In async block");
    });
}

Async/Await Syntax

Futures and Await

use tokio::time::sleep;
use std::time::Duration;

async fn slow_operation() -> String {
    sleep(Duration::from_secs(1)).await;
    "result".to_string()
}

#[tokio::main]
async fn main() {
    let result = slow_operation().await;
    println!("{}", result);
}

Spawning Tasks

#[tokio::main]
async fn main() {
    // Spawn background task
    tokio::spawn(async {
        for i in 0..5 {
            println!("Background task: {}", i);
            tokio::time::sleep(Duration::from_millis(100)).await;
        }
    });

    // Main task continues
    for i in 0..3 {
        println!("Main task: {}", i);
        tokio::time::sleep(Duration::from_millis(200)).await;
    }

    // Wait for background task
    tokio::time::sleep(Duration::from_millis(1000)).await;
}

// Output:
// Main task: 0
// Background task: 0
// Background task: 1
// Main task: 1
// ...

Joining Multiple Tasks

#[tokio::main]
async fn main() {
    let task1 = tokio::spawn(async {
        sleep(Duration::from_secs(1)).await;
        "result 1"
    });

    let task2 = tokio::spawn(async {
        sleep(Duration::from_secs(2)).await;
        "result 2"
    });

    let (r1, r2) = tokio::join!(task1, task2);
    println!("{:?}, {:?}", r1, r2);
}

Tokio Networking

TCP Server

use tokio::net::TcpListener;
use tokio::io::{AsyncWriteExt, AsyncReadExt};

#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:8000")
        .await
        .expect("Failed to bind");

    loop {
        let (mut socket, addr) = listener
            .accept()
            .await
            .expect("Failed to accept");

        // Spawn task for each connection
        tokio::spawn(async move {
            let mut buf = vec![0; 1024];
            loop {
                let n = socket.read(&mut buf).await.unwrap();
                if n == 0 { return; }
                
                socket.write_all(&buf[..n]).await.unwrap();
            }
        });
    }
}

TCP Client

use tokio::net::TcpStream;
use tokio::io::{AsyncWriteExt, AsyncReadExt};

#[tokio::main]
async fn main() {
    let mut stream = TcpStream::connect("127.0.0.1:8000")
        .await
        .expect("Failed to connect");

    // Send data
    stream.write_all(b"Hello").await.unwrap();

    // Read response
    let mut buf = vec![0; 1024];
    let n = stream.read(&mut buf).await.unwrap();
    println!("Received: {}", String::from_utf8_lossy(&buf[..n]));
}

Concurrency Primitives

Mutex (Lock)

use tokio::sync::Mutex;
use std::sync::Arc;

#[tokio::main]
async fn main() {
    let counter = Arc::new(Mutex::new(0));

    let mut tasks = vec![];

    for _ in 0..10 {
        let c = Arc::clone(&counter);
        tasks.push(tokio::spawn(async move {
            for _ in 0..100 {
                let mut num = c.lock().await;
                *num += 1;
            }
        }));
    }

    for task in tasks {
        task.await.unwrap();
    }

    println!("Counter: {}", *counter.lock().await);
}

Channel (Communication)

use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel(100);

    // Producer task
    tokio::spawn(async move {
        for i in 0..10 {
            tx.send(i).await.unwrap();
        }
    });

    // Consumer task
    while let Some(msg) = rx.recv().await {
        println!("Received: {}", msg);
    }
}

Broadcast (Pub/Sub)

use tokio::sync::broadcast;

#[tokio::main]
async fn main() {
    let (tx, _) = broadcast::channel(10);

    // Publisher
    for i in 0..5 {
        tx.send(i).unwrap();
    }

    // Multiple subscribers
    let mut rx1 = tx.subscribe();
    let mut rx2 = tx.subscribe();

    tokio::spawn(async move {
        while let Ok(msg) = rx1.recv().await {
            println!("Sub1: {}", msg);
        }
    });

    tokio::spawn(async move {
        while let Ok(msg) = rx2.recv().await {
            println!("Sub2: {}", msg);
        }
    });

    tokio::time::sleep(Duration::from_millis(100)).await;
}

Real-World Example: HTTP Echo Server

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

#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:8000")
        .await
        .unwrap();

    println!("Listening on 127.0.0.1:8000");

    loop {
        let (socket, addr) = listener.accept().await.unwrap();
        println!("New connection: {}", addr);

        tokio::spawn(async move {
            handle_connection(socket).await;
        });
    }
}

async fn handle_connection(socket: tokio::net::TcpStream) {
    let (reader, mut writer) = socket.into_split();
    let mut lines = BufReader::new(reader).lines();

    let mut headers = vec![];
    while let Ok(Some(line)) = lines.next_line().await {
        if line.is_empty() { break; }
        headers.push(line);
    }

    if let Some(request_line) = headers.first() {
        let response = format!(
            "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello"
        );
        writer.write_all(response.as_bytes()).await.ok();
    }
}

Performance Characteristics

Benchmark Results (1M concurrent connections)

Metric                      Tokio
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Memory per connection       ~500 bytes
Task switch latency         <1 microsecond
Context switches/sec        10,000-50,000
P99 latency (echo)          < 100 microseconds
Throughput (echo)           > 1M req/sec
CPU cores needed            4-8

Scaling Patterns

Connections  Threads  Tokio (CPU)  Memory
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1K           1        1%           20MB
10K          10       5%           200MB
100K         100      15%          2GB
1M           1000     30-50%       20GB  <- Not practical
1M (async)   4        40-60%       500MB  <- Tokio excels

Debugging Async Code

Logs and Tracing

use tracing::{info, error, span, Level};

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    let span = span!(Level::INFO, "process");
    let _enter = span.enter();

    info!("Starting operation");
    
    match operation().await {
        Ok(result) => info!("Success: {}", result),
        Err(e) => error!("Failed: {}", e),
    }
}

async fn operation() -> Result<String, String> {
    Ok("Done".to_string())
}

Timeouts

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

#[tokio::main]
async fn main() {
    let result = timeout(
        Duration::from_secs(5),
        slow_operation()
    ).await;

    match result {
        Ok(Ok(data)) => println!("Success: {}", data),
        Ok(Err(e)) => println!("Operation error: {}", e),
        Err(_) => println!("Timeout!"),
    }
}

async fn slow_operation() -> Result<String, String> {
    tokio::time::sleep(Duration::from_secs(10)).await;
    Ok("Done".to_string())
}

Glossary

  • Async Runtime: System that schedules and executes async tasks
  • Future: Represents a value that may not be ready yet
  • Await: Syntax to wait for a Future to complete
  • Executor: Component that runs async tasks
  • Work Stealing: Scheduler technique for load balancing
  • Reactor: I/O multiplexing system (epoll/kqueue)

Resources


Comments