Async I/O
Tokio wraps OS async I/O (epoll, kqueue, IOCP) for sockets and files. Use AsyncRead/AsyncWrite traits and buffered helpers for efficient network and file code.
Recipe
Quick-reference recipe card - copy-paste ready.
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut stream = TcpStream::connect("127.0.0.1:8080").await?;
stream.write_all(b"GET /\r\n\r\n").await?;
let mut buf = vec![0u8; 1024];
let n = stream.read(&mut buf).await?;
println!("read {n} bytes");
Ok(())
}When to reach for this: TCP/UDP servers, HTTP clients, file streaming, and any socket-based protocol.
Working Example
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::fs::File;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let file = File::open("/etc/hosts").await?;
let mut reader = BufReader::new(file);
let mut line = String::new();
while reader.read_line(&mut line).await? > 0 {
if line.starts_with('#') {
line.clear();
continue;
}
println!("{}", line.trim());
line.clear();
}
Ok(())
}What this demonstrates:
tokio::fs::Filefor async file open.BufReaderreduces syscall count for line reads.read_linereturns 0 at EOF.
Deep Dive
How It Works
- Reactor: OS notifies Tokio when sockets/files are readable/writable.
- AsyncRead/AsyncWrite:
poll_read/poll_writeintegrate with wakers. - Buffered I/O:
BufReader/BufWriterbatch small reads/writes. - Split:
split()separates read/write halves for concurrent directions.
Common Types
| Type | Purpose |
|---|---|
TcpListener / TcpStream | TCP servers and clients |
UdpSocket | Datagram protocols |
tokio::fs | Async filesystem |
tokio::io::copy | Stream pump between reader/writer |
Rust Notes
use tokio::io::copy;
use tokio::fs::File;
async fn copy_file(src: &str, dst: &str) -> std::io::Result<u64> {
let mut reader = File::open(src).await?;
let mut writer = File::create(dst).await?;
copy(&mut reader, &mut writer).await
}- Prefer
reqwest/hyperfor HTTP instead of raw TCP for application code. - Set TCP
nodelayand buffer sizes for latency-sensitive services. - Use
tokio::net::lookup_hostfor async DNS.
Gotchas
- Small read loops without buffer - Syscall per byte kills throughput. Fix:
BufReaderor larger fixed buffers. - std::fs in async handler - Blocks worker. Fix:
tokio::fsorspawn_blocking. - Forgotten flush on BufWriter - Data stuck in buffer. Fix:
flush().awaitbefore close. - Accept loop without spawn - Serializes connections. Fix:
spawnper connection or use Axum. - UDP recv buffer too small - Truncates datagrams. Fix: Size buffer to MTU/application max.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Axum/hyper | HTTP APIs | Custom binary protocol |
quinn | QUIC | Plain TCP requirement |
mmap + sync | Large static files | Many concurrent small reads |
| Blocking std in thread | Legacy only | Tokio worker hot path |
FAQs
tokio::fs vs std::fs?
tokio::fs uses blocking pool under the hood for some ops but presents async API - still better than calling std on workers directly.
AsyncRead in traits?
Use tokio::io::AsyncRead + async_trait or generic bounds in library APIs.
Half-close TCP?
shutdown() on stream halves - protocol dependent for HTTP keep-alive.
Unix sockets?
tokio::net::UnixStream mirrors TCP async API on Unix platforms.
TLS?
tokio-rustls wraps streams with async handshake and read/write.
Backpressure?
Stop calling read when downstream is full; use bounded channels between stages.
copy buffer size?
Default 8KB in copy - tune for high-throughput pipelines.
stdio async?
tokio::io::stdin/stdout available - often dedicated thread for interactive CLI.
Testing I/O?
Bind 127.0.0.1:0 for ephemeral port; use tokio::test with real sockets.
Axum relation?
Axum uses hyper + Tokio I/O - rarely need raw TcpStream in web handlers.
Related
- Tokio Basics - TCP example
- Streams - byte streams as Stream
- Blocking in Async - file I/O pitfalls
- select! & Concurrency - I/O multiplexing
- Graceful Shutdown - draining connections
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.