nexio
A thread-per-core async runtime with owned-buffer I/O: io_uring on Linux, and a portable
epoll / kqueue / IOCP backend everywhere else.
- Owned-buffer I/O. You hand a buffer to an operation and get it back with the result, so the kernel never writes into memory Rust thinks it still owns.
- Cancellation-safe by construction. Dropping a future that has an operation in flight does not free its buffer. The runtime keeps it until the kernel's completion arrives, so there is no use-after-free window. This layer is checked with Miri and Kani (see Verification).
- One reactor per thread, no work-stealing. Tasks are
!Send. Scale across cores withrun_sharded: one independent reactor per core, connections spread bySO_REUSEPORT. - Small surface: TCP, UDP, files, timers, channels,
select/join,JoinSet.
Status: pre-release. The io_uring backend (Linux) is the verified one, published as 0.1. The portable
backend (Windows, macOS, other Unix, and Linux with --features poll) is new and covers TCP and timers
so far; see Backends for exactly what each one has.
Quick start
[]
= "0.2"
use TcpListener;
More in examples/: echo_server, echo_server_multicore, file_and_timeout.
Use every core
run_sharded;
Each shard gets its own io_uring and its own executor, pinned to its own CPU. TcpListener::bind enables
SO_REUSEPORT, so the kernel balances new connections across shards.
Requirements
- Linux with
io_uring. The full test suite and a soak test pass on vendor kernels 5.10 (Debian 11), 5.15 (Ubuntu 22.04 LTS) and 6.1 (Debian 12) in KVM guests and on 6.12 on the host. nexio enablesCOOP_TASKRUN,DEFER_TASKRUNandSINGLE_ISSUER(6.1+) when available and falls back to a plain ring on older kernels. Vendors backport io_uring changes, so this does not prove every mainline version in between; mainline 5.11 to 5.14 and other distributions' kernels have not been run. - Some container runtimes block
io_uringwith seccomp; if ring setup fails, that is why. - Declared MSRV is Rust 1.75; it has only been built and tested on current stable.
- Any other target uses the portable backend automatically (see below).
Backends
The backend is chosen at build time. The public API is the same; the 9 tests in tests/portable_api.rs
run unchanged against every backend. They pass on io_uring, Linux epoll, Windows and macOS.
io_uring backend |
Portable backend | |
|---|---|---|
| Used on | Linux (default) | Windows, macOS, other Unix, and Linux with --features poll |
| Mechanism | completion-based io_uring, owned buffers held by the kernel |
readiness polling (epoll / kqueue / IOCP via the polling crate) with non-blocking socket2 sockets |
TCP, timers, channels, select/join, spawn |
yes | yes |
| UDP | yes | yes |
| Async file I/O | yes | yes (runs on a blocking thread pool) |
| Unix-domain sockets | yes (Unix) | yes (Unix) |
Signals (ctrl_c, Unix signal) |
yes | yes |
| Child processes | yes | yes |
| Async host-name resolution | yes (blocking pool) | yes (blocking pool) |
SO_REUSEPORT sharding with run_sharded |
yes | Unix only; not available on Windows |
| Cancellation safety | completion slab, checked by Miri and Kani | nothing is ever handed to the kernel, so dropping a pending future just abandons the wait |
| Tested | Linux 5.10 / 5.15 / 6.1 / 6.12, soak, fuzz, Miri, Kani | Windows, macOS (CI), Linux epoll; no soak or fuzz run on it yet |
The portable backend gives you the same API on every platform, not io_uring's speed: it is an ordinary
readiness reactor. Use --features poll on Linux if io_uring is blocked (some container runtimes
disable it).
Performance, honestly
Measured on one 2-vCPU Oracle Cloud VM (Linux 6.12), loopback, one machine. Full methodology, raw numbers
and every correction we made are in reports/split_benchmark_results.md.
- TCP echo (server pinned to one core, byte-verifying client on another): nexio uses about 6.75 µs of server CPU per message versus 7.1–7.2 (compio, monoio) and 7.6–7.85 (tokio). That is roughly 5% cheaper than compio/monoio and 12% cheaper than tokio.
- File I/O: about 5–10x tokio (which uses a blocking thread pool); level with compio and monoio.
- Batched random reads (1M, page cache): about 1.46M IOPS versus 1.27–1.34M (compio) and 1.36–1.46M (monoio).
Please read these as: nexio is competitive with the other io_uring runtimes and slightly ahead on these tests, not dramatically faster. On plain TCP, about 93% of server CPU is the kernel's TCP stack, which no user-space runtime can remove. We tried registered buffers, multishot accept/recv, zero-copy sends and batching; the results, including the ones that did not help, are in the reports. Results on other hardware, real NICs and higher core counts may differ.
Verification
The cancellation-safety layer is the part most likely to hide memory-safety bugs, so it is the part that is checked the hardest:
- Kani model-checks the completion slab's index arithmetic: 9 harnesses, run with
cargo kani --tests. - Miri (strict provenance) runs the pure-Rust slab and cancellation tests:
bash scripts/ci_miri.sh. - Integration tests force submission-queue exhaustion,
-ECANCELEDraces and fd-leak scenarios on a real kernel:cargo test. - Fuzzing: a coverage-guided target drives random insert/complete/drop/garbage-CQE sequences through
the slab and checks a model (no early drop, no double drop, no leak). 3.5M executions, no crashes; a
deliberately injected leak was caught in seconds.
cargo +nightly fuzz run slab_ops. - Soak: 10 minutes of connection churn with random cancellation (2.0M byte-verified echoes, 337k
cancelled in-flight reads): 0 mismatches, flat memory, descriptors back to baseline.
cargo run --release --example soak -- 600 128. - Kernel matrix: tests plus soak inside real 5.10, 5.15 and 6.1 kernels:
scripts/kernel_matrix/run.sh.
Details and limits: reports/reliability_results.md.
Limits: Miri and Kani cannot execute io_uring syscalls, so they cover the pure-Rust bookkeeping, not
the kernel interaction. Cancellation of an in-flight io_uring operation is best-effort at the kernel
level; nexio's guarantee is that memory stays valid until the kernel is done with it.
Optional features
| Feature | Adds |
|---|---|
tokio-compat |
nexio::compat::Compat: use nexio TCP/Unix streams through tokio's AsyncRead/AsyncWrite (no tokio runtime needed) |
tls |
nexio::tls: TlsConnector/TlsAcceptor/TlsStream over rustls (ring provider). Truncated streams are an UnexpectedEof error, never a clean end |
http |
nexio::http: HTTP/1.1 client and server on hyper (HTTPS by passing a TlsStream) |
poll |
Force the portable backend on Linux |
nexio contains no cryptography and no HTTP parser of its own: those are rustls and hyper, wired to
nexio's sockets. HTTP/2 is not provided, because hyper's HTTP/2 needs Send tasks and nexio's are
thread-local by design.
What is not here
- HTTP/2 and HTTP/3 (see above), WebSockets, and a trust store for TLS (you pick the roots, for example
webpki-roots). - The portable backend (Windows, macOS, other Unix) is verified by tests and CI but has not had the soak,
fuzz and kernel-matrix campaigns the
io_uringbackend has had. Files there use a blocking thread pool.
See ROADMAP.md.
Building and testing
License
MIT OR Apache-2.0.