Monoio: a thread-per-core Rust runtime built on io_uring, epoll and kqueue
Rust async runtime based on io-uring.
At a glance
- What is it?
- Monoio is a Rust async runtime for servers that stay on one thread per core, so tasks never need Send or Sync. It is a real architectural bet with real portability costs.
- Who is it for?
- Adopt Monoio if you are writing a Linux or macOS server whose work is io-bound on network sockets and whose per-core load is roughly even, and if you can accept that tasks are not Send or Sync. Do not adopt it if you need Windows today, if your workload is unbalanced enough that cores would idle, or if your codebase is built around Tokio traits and third-party crates that assume them.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 73 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What Monoio solves, and who it is actually for
Tokio's scheduler moves tasks between worker threads. That is a good default, and it is why a Tokio task must be Send, and why shared state usually ends up behind an Arc with a lock. Monoio takes the opposite position. The README describes it as a thread-per-core runtime: a task stays on the thread that spawned it, and the documentation states that users do not need to worry about tasks being Send or Sync, because thread local storage can be used safely and data does not escape the thread across await points.
That removes a whole class of ceremony. You stop paying for Arc, Mutex and cross-thread handoff when the data never had to cross a thread in the first place. The README names the target use case directly: a load balancer written like NGINX, where per-connection state belongs to one core and sharing it between cores buys nothing. The stated audience is servers doing io-bound work on network sockets, where native asynchronous I/O is what raises throughput.
This is a narrower audience than it first appears. If your service is CPU-bound, or if you rely on work stealing to smooth out uneven request costs, the model works against you rather than for you.
How the runtime is put together: drivers, io_uring and the rent IO traits
Monoio is a pure io_uring, epoll and kqueue runtime. The README is explicit that it does not run on top of another runtime, unlike Tokio-uring, and calls that the reason it is more efficient. There is no hidden executor underneath; the reactor and the driver are the runtime.
Driver selection is platform-driven. On Linux 5.6 or newer, Monoio can use uring or epoll as the io driver. Below that kernel version it can only run in epoll mode, and on macOS it uses kqueue. The README lists other platforms as currently unsupported, with experimental Windows support described as on the way. There is a legacy driver path documented for Linux and macOS when the kernel does not qualify.
The IO abstraction is where Monoio diverges most from what Rust developers expect. The example imports AsyncReadRent and AsyncWriteRentExt, and the read call looks like this: `(res, buf) = stream.read(buf).await;`. The buffer is moved into the future and handed back with the result. That is a deliberate consequence of completion-based I/O: io_uring owns the buffer while the operation is in flight, so the runtime cannot let you keep a mutable reference to it. The README acknowledges the cost, saying the new IO abstraction may cause some compatibility problems. It does. Code written against the standard AsyncRead and AsyncWrite traits does not drop in here, and that is the main reason the monoio-compat crate exists in the workspace.
Installing Monoio and running the echo example
The README states you need Rust 1.75, and asks you to make sure it is the latest version. For io_uring you also need a kernel that supports it, 5.6 or newer per the platform support document, and memlock configured as a proper number per docs/en/memlock.md. If the kernel does not qualify, the legacy driver is the documented fallback.
Start by adding the crate to a project:
cargo add monoioThe README's basic example is an echo server. It binds a listener, accepts connections in a loop, and spawns a task per connection with `monoio::spawn`. The entry point is annotated with the `#[monoio::main]` attribute macro, which comes from the monoio-macros crate in this workspace.
use monoio::io::{AsyncReadRent, AsyncWriteRentExt};
use monoio::net::{TcpListener, TcpStream};
#[monoio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:50002").unwrap();
println!("listening");
loop {
let incoming = listener.accept().await;
if let Ok((stream, addr)) = incoming {
println!("accepted a connection from {}", addr);
monoio::spawn(echo(stream));
}
}
}The README says to run the example and then connect with `nc 127.0.0.1 50002` in another shell, where all input is echoed back. The echo function reads into a `Vec<u8>` and writes it back, reassigning `buf` from the read and write futures each time, which is the buffer-return pattern that the rent traits impose.
The workspace also ships a builder, so you are not limited to the attribute macro. `examples/builder.rs` shows the runtime being constructed directly, which is the path to take when you want to choose the driver explicitly rather than let the macro decide. Other examples in the repository cover a proxy, Unix domain sockets, timers, HTTP/2 and Hyper integration, which is a better starting point than the echo server if you are evaluating it for a real service.
Where Monoio is the wrong tool
The README's own limitations section is the honest place to start. The first entry is platform coverage: other platforms are currently not supported, and Windows is experimental. If you ship to Windows, this is not a runtime you can adopt today.
The second entry is more interesting because it is a scheduling critique, not a portability one. The README states that if the workload is very unbalanced, Monoio may perform worse than Tokio, because CPU cores may not be fully utilized. That follows directly from thread-per-core. With work stealing, a core that finishes its queue pulls work from a busy core. Without it, a core with one expensive request sits idle while its neighbours drain their queues. A service with a mix of fast cache hits and slow database calls is exactly the shape that suffers.
There is a third cost the README does not put in the limitations list but does mention in the design section: enabling unstable Rust features and designing a new IO abstraction, which it says may cause compatibility problems. That is a real adoption tax. Every library that implements AsyncRead or AsyncWrite against the standard traits needs a wrapper or a rewrite before it fits. The monoio-compat crate in the workspace is the bridge, but compatibility layers are a boundary you have to test rather than assume.
None of this makes Monoio a bad choice. It makes it a choice with a narrow shape, and the shape is stated plainly.
Monoio compared with Tokio and Glommio
The comparison people search for is Monoio versus Tokio, and the difference is not performance tuning. It is the scheduling model. Tokio is a work-stealing multi-threaded runtime: tasks are Send, they can migrate between worker threads, and shared state is normally wrapped in Arc and a lock. Monoio is thread-per-core: a task stays put, so Send and Sync bounds disappear and thread local storage becomes usable without the usual caveats. Tokio also runs on top of epoll, kqueue and, through Tokio-uring, a separate io_uring integration, whereas Monoio is itself the io_uring, epoll and kqueue runtime. The README makes that point directly when it contrasts itself with Tokio-uring.
Glommio is the other thread-per-core runtime in this space, and the resemblance is real: both reject work stealing, both push you toward per-core state. The distinction that matters in Monoio's own documentation is the IO abstraction. Monoio's read and write take the buffer by value and return it, which is a completion-based model matching io_uring semantics; the README frames the whole IO design as new and warns about the compatibility fallout. Glommio is not mentioned anywhere in this repository's README, so a detailed feature-by-feature comparison is not something this material supports. What it does support is the architectural split: thread-per-core with a completion-based IO interface, versus a work-stealing runtime with the standard futures traits.
If your code is already Tokio-shaped, the migration is not a flag change. It is a rewrite of the IO layer.
Maintenance, release cadence and the licence you are accepting
The repository is not archived, and the last push was on 2026-07-20. The most recent release listed is 0.2.4 from 2024-08-20, preceded by 0.2.3 in March 2024 and 0.2.2 in February 2024. So the commit history is more recent than the published crate version by a wide margin. That gap is worth understanding before you pin a dependency: if you need a fix that landed after 0.2.4, you are looking at a git dependency or a wait, not a crates.io version bump.
The version number itself is a signal. Monoio is still on 0.2.x, which in Cargo's semver rules means every minor bump can break you. The README also says HTTP and RPC frameworks are on the way, and lists local-sync, monoio-tls and monoio-codec as associated projects rather than as part of the core crate. Budget for the surrounding pieces being separate dependencies with their own release timing.
Licensing is straightforward on paper: the README states Monoio is licensed under the MIT license or Apache license, and the repository carries LICENSE-MIT and LICENSE-APACHE at the top level. There is also a LICENSE-THIRD-PARTY file, which is consistent with the README's note that the project referenced Tokio, Mio, Tokio-uring and other projects during development. If your organisation runs licence review, that third-party file is the one to read, not just the dual-licence badge. This is a description of what the repository contains, not legal advice.
What to verify before you commit to Monoio
Three things are worth checking in order, because each one can end the evaluation.
First, the kernel. Run `uname -r` and compare against the 5.6 threshold the README cites for io_uring. Below it you are on the epoll path, which the README documents as the only option on older Linux, and the io_uring advantage you came for is not there. Then read docs/en/memlock.md and check your memlock limit, because the README treats it as a precondition rather than an optional tuning step.
Second, the workload shape. Ask whether per-core load is roughly even. If a single request can be ten times more expensive than another, thread-per-core will leave cores idle and the README says so. That is a design boundary, not a bug you can configure away.
Third, the dependency graph. The rent-style read and write traits mean third-party crates built on AsyncRead and AsyncWrite need monoio-compat or a rewrite. Count how many of those you depend on before you count the performance win. The examples directory gives you a faster read on real usage than the README's echo server: examples/proxy.rs, examples/h2_server.rs and examples/hyper_server.rs show the runtime under workloads closer to production.
Editorial conclusion
Adopt Monoio if you are writing a Linux or macOS server whose work is io-bound on network sockets and whose per-core load is roughly even, and if you can accept that tasks are not Send or Sync. Do not adopt it if you need Windows today, if your workload is unbalanced enough that cores would idle, or if your codebase is built around Tokio traits and third-party crates that assume them. Before committing, check your kernel version against the 5.6 threshold, read docs/en/memlock.md and confirm your memlock limit, and decide whether monoio-compat is enough to bridge the Tokio ecosystem code you already depend on.
Frequently asked questions
How does Monoio differ from Tokio?
Monoio is a thread-per-core runtime, so tasks stay on the thread that spawned them and do not need to be Send or Sync. Tokio uses a work-stealing scheduler where tasks can migrate between worker threads, which is why Tokio tasks carry those bounds. Monoio is also a pure io_uring, epoll and kqueue runtime rather than something running on top of another runtime.
What Rust version and kernel does Monoio require?
The README states you need Rust 1.75 and asks you to make sure it is the latest version. For io_uring, the kernel must support it, which the platform support document puts at 5.6 or newer. Below that, Monoio can only run in epoll mode on Linux.
Does Monoio work on Windows or macOS?
On macOS, kqueue can be used. Windows support is described in the README as experimental and on the way, and the limitations section lists other platforms as currently not supported.
Why do Monoio's read and write methods take the buffer by value?
The runtime uses a completion-based IO abstraction, so the buffer is moved into the future and returned with the result, as in `(res, buf) = stream.read(buf).await;`. The README notes this new IO abstraction may cause some compatibility problems, which is why code written against the standard AsyncRead and AsyncWrite traits does not fit directly.
When is Monoio slower than Tokio?
The README states that if the workload is very unbalanced, Monoio may cause performance degradation compared with Tokio, because CPU cores may not be fully utilized. Without work stealing, a core with a long-running task cannot hand its queue to an idle core.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/monoio-rs-monoio)