Ditching async/await: A Look at Low-Level I/O with Mio


Ever wondered how high-performance servers like Redis handle thousands of connections on a single thread? The secret lies in a low-level event loop, and in Rust, the tool for that job is Mio.
Mio is a fast, low-level I/O library for Rust that focuses on non-blocking APIs and event notification. It’s built for developing high-performance I/O applications with minimal overhead over the operating system’s native abstractions. Think of it as a very, very barebones version of Tokio, without any of the async/await ergonomics.
I came across Mio while trying to build an event-based loop for my own Redis-like key-value store in Rust. Redis is famously single-threaded yet incredibly performant, thanks to its event loop architecture. To mimic that behavior, I needed a low-level, fine-grained way to handle I/O — and Mio was the perfect fit.
Features
Non-blocking I/O: Supports non-blocking reads and writes for TCP and UDP.
Event notification: Wraps epoll (Linux), kqueue (macOS), IOCP (Windows) in a uniform API.
Minimal abstraction: Gives you raw access with zero-cost abstractions — no thread pools, tasks, or futures.
Portable: Works across Unix and Windows systems with the same API.
Foundation for async: Tokio, for example, is built on top of Mio.
Use cases
Building TCP and UDP servers: Ideal for high-performance networking applications.
Implementing an event loop: Like in Redis, allowing thousands of concurrent connections with a single thread.
Creating custom async runtimes: If you want to experiment with your own concurrency models or learn how things work under the hood.
Learning systems-level I/O: Great for understanding how event-driven systems work at the OS level.
Limitations
No async/await support: You’ll need to manage all state and I/O readiness manually.
Low-level and verbose: Requires more boilerplate than higher-level frameworks.
No built-in runtime or task scheduling: You write everything yourself.
Here’s what a minimal sketch of how a Mio-based TCP server looks like:
use mio::{Events, Interest, Poll, Token};
use mio::net::TcpListener;
use std::collections::HashMap;
fn main() -> std::io::Result<()> {
let mut poll = Poll::new()?;
let mut events = Events::with_capacity(128);
let addr = "127.0.0.1:9000".parse().unwrap();
let mut server = TcpListener::bind(addr)?;
// Use Token(0) for the server
const SERVER: Token = Token(0);
poll.registry().register(&mut server, SERVER, Interest::READABLE)?;
// A simple counter for generating unique tokens for clients
let mut unique_token_counter = 1;
loop {
poll.poll(&mut events, None)?;
for event in &events {
match event.token() {
SERVER => loop {
// The server is ready to accept new connections.
// We use a loop because we might receive multiple connections at once.
match server.accept() {
Ok((mut connection, address)) => {
println!("Accepted new connection from: {}", address);
// Generate a new token for the client
let client_token = Token(unique_token_counter);
unique_token_counter += 1;
// **Register the new connection with Poll**
poll.registry().register(
&mut connection,
client_token,
Interest::READABLE.add(Interest::WRITABLE),
)?;
}
// If we get a "WouldBlock" error, we've accepted all pending connections.
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
break;
}
Err(e) => {
return Err(e); // A real error occurred
}
}
},
token => {
// This is where you would handle events for a specific client,
// like reading their sent data.
println!("Got event for client token: {:?}", token);
}
}
}
}
}
So, when should you reach for mio ?
Mio is for the builder who needs to get their hands on the raw engine. While frameworks like Tokio provide a comfortable, powerful car with automatic transmission (async/await
), Mio gives you the gearbox, the pistons, and the chassis. You reach for it when:
You need absolute, fine-grained control over the event loop.
You're building a high-performance system, like a database or network proxy, where every nanosecond of overhead counts.
You want to build your own async runtime or understand how they work under the hood.
Or you just don’t like things done for you and want to do it yourself!
In my journey to build a Redis-like server, Mio was the perfect choice because it provides the fundamental, OS-level event notifications without imposing any other opinions. It's not the easiest path, but it's the one that gets you closest to the metal.
Subscribe to my newsletter
Read articles from Khushal Agrawal directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
