Tokio is an event-driven, non-blocking I/O runtime for the Rust programming language. It's aimed at developers building network servers, clients, or any application that needs to handle many concurrent I/O operations without spawning a thread per connection. If you're writing async Rust, Tokio is the runtime most of the ecosystem (axum, hyper, tonic, warp, tower) is built on top of.
tokio::net.#[tokio::main] and #[tokio::test] reduce boilerplate for setting up the runtime in binaries and tests.tokio-util, tokio-stream, tokio-test, and tokio-macros extend the core runtime with streams, testing helpers, and codec utilities.Tokio fits applications that need to manage a large number of concurrent connections or I/O-bound tasks efficiently: HTTP servers, gRPC services, proxies, chat servers, database drivers, and CLI tools that talk to many network endpoints at once. It's the base layer for frameworks like axum and tonic, so if you're building on those, you're already using Tokio.
It also works well for background job systems that need timers, cancellation, and structured concurrency without managing raw OS threads.
Tokio is not the right choice for simple scripts or CPU-bound batch processing where there's no I/O concurrency to exploit; a synchronous Rust program or a thread pool may be simpler and just as fast. It also adds complexity (async/await, Send/Sync bounds, pinning) that isn't worth it for small single-purpose tools with minimal concurrency needs.
Add Tokio to your Cargo.toml. Enable the full feature set to get everything (I/O, macros, runtime, time, sync, etc.) or pick individual features to keep your binary lean:
[dependencies]
tokio = { version = "1", features = ["full"] }
Then write an async main function using the #[tokio::main] macro:
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
loop {
let (mut socket, _) = listener.accept().await?;
tokio::spawn(async move {
let mut buf = [0; 1024];
loop {
let n = match socket.read(&mut buf).await {
Ok(0) => return,
Ok(n) => n,
Err(_) => return,
};
if socket.write_all(&buf[0..n]).await.is_err() {
return;
}
}
});
}
}
If you want a smaller binary, swap "full" for specific features like ["rt-multi-thread", "net", "macros"] and check the feature-flag docs on docs.rs for the exact list you need. For a fixed, long-term-supported version, pin to an LTS minor release using a tilde requirement, e.g. tokio = { version = "~1.47", features = [...] }.