tokio-rs/io-uring: Low-Level Linux I/O in Rust
The `io_uring` library for Rust
At a glance
- What is it?
- tokio-rs/io-uring provides direct Rust access to Linux io_uring queues and operations. It fits developers who need explicit control over file descriptors, buffers, submissions, and completions, at the cost of managing low-level safety and kernel constraints directly.
- Who is it for?
- Adopt tokio-rs/io-uring when a Linux Rust application needs direct io_uring access and can manage descriptor and buffer lifetimes through its unsafe API. Do not choose it for a simple file read without a specific need for queue-based I/O, and do not assume every opcode works on older kernels.
- 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 46 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 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Why tokio-rs/io-uring Targets Low-Level Linux I/O
tokio-rs/io-uring is a Rust interface for the Linux io_uring userspace mechanism. It is aimed at developers building filesystem, network, or asynchronous systems that need to submit operations to an I/O ring and inspect completion entries directly. The repository describes the crate as a low-level interface rather than a complete application framework.
That distinction matters. The public workflow exposes the file descriptor, operation opcode, buffer address, buffer length, submission queue, and completion result. Applications decide how to associate those operations with their own state through a value such as `user_data`. There is no documented high-level file wrapper that owns the entire buffer lifetime or a documented async runtime that schedules completion handlers.
This low-level design can fit code that needs precise control over submission and completion. It is a poor fit for a routine read when the standard Rust filesystem API already meets the requirement. The additional queue protocol and unsafe resource requirements do not create value merely because io_uring exists. The intended user is therefore a Rust developer operating close to Linux and prepared to preserve the resources referenced by every submitted operation.
How Submission and Completion Entries Move Through io-uring
The README demonstrates a single file read through `IoUring`. The program creates a ring with `IoUring::new(8)`, opens `README.md`, and allocates a 1024-byte buffer. It builds an `opcode::Read` containing the raw file descriptor, a pointer to the buffer, and the buffer length. The entry receives the application-defined value `0x42` through `user_data`.
The entry is pushed into the submission queue with `ring.submission().push(&read_e)`. Submission occurs through `ring.submit_and_wait(1)`, which asks the ring to submit pending operations and wait for one completion. The example then takes an entry from the completion queue, confirms that its `user_data` is `0x42`, and checks that `result()` is nonnegative.
This separates operation creation from result handling. `user_data` gives the application a way to correlate a completion with known state, while `result()` carries the operation-specific outcome. A negative result in the example represents an I/O error rather than the number of bytes read.
The buffer remains a caller-owned resource. Its address is passed as a pointer, so it must stay at a valid address and retain sufficient storage until submission is complete and the completion has been processed. The raw file descriptor must likewise remain valid for the relevant operation. The crate does not take ownership of the `Vec` or the `File` in the example. This makes the API compact, but it also moves lifetime discipline into application code.
Installing io-uring and Running a First File Read
The README tells users to declare the crate as a Cargo dependency. This configuration selects version 0.7 through Cargo's version requirement. The repository manifest identifies the package as version 0.7.15.
Add the dependency to `Cargo.toml`:
[dependencies]
io-uring = "0.7"With `README.md` present in the working directory, the following program submits one read and checks its completion. It should return `Ok(())` when the ring is initialized, the file opens, the operation completes, and the completion reports a nonnegative result.
use io_uring::{opcode, types, IoUring};
use std::os::unix::io::AsRawFd;
use std::{fs, io};
fn main() -> io::Result<()> {
let mut ring = IoUring::new(8)?;
let fd = fs::File::open("README.md")?;
let mut buf = vec![0; 1024];
let read_e = opcode::Read::new(types::Fd(fd.as_raw_fd()), buf.as_mut_ptr(), buf.len() as _)
.build()
.user_data(0x42);
unsafe { ring.submission().push(&read_e).expect("submission queue is full"); }
ring.submit_and_wait(1)?;
let cqe = ring.completion().next().expect("completion queue is empty");
assert_eq!(cqe.user_data(), 0x42);
assert!(cqe.result() >= 0, "read error: {}", cqe.result());
Ok(())
}The README also documents commands for the repository's test and benchmark packages:
cargo run --package io-uring-test
cargo bench --package io-uring-benchThe first command runs the test package, while the second runs the benchmark package. The README does not define expected console output for either command.
Linux 5.6 and Lifetime Failure Boundaries
The documented `Read` example has a specific kernel floor. The README states that opcode `Read` is available only after Linux kernel 5.6 and that the example fails on an older kernel. This means an application supporting an older kernel cannot copy the example without checking compatibility.
The more frequent integration risk is resource lifetime. Passing `buf.as_mut_ptr()` does not make the vector owned by the ring. If the vector is moved in a way that reallocates its storage, or dropped before completion processing, the submitted address can become invalid. The `File` must also remain alive while its descriptor is used by the operation. These requirements explain why submission occurs inside an `unsafe` block and why the README explicitly calls out validity.
Queue state creates another boundary. The sample treats a full submission queue as a panic through `expect("submission queue is full")`, and it treats a missing completion as a panic through `expect("completion queue is empty")`. That is acceptable for a short introduction, but production code will need application-specific behavior if a queue fills, initialization fails, or a completion arrives in an unexpected order.
The crate should not be selected merely because an application performs file or socket I/O. Its extra control is useful only when queue-based submission addresses a real design need. If the application cannot define buffer ownership and completion lifetime rules, the API's flexibility becomes a source of undefined behavior rather than a benefit.
Features, Maintenance State, and Upgrade Boundaries
The root `Cargo.toml` declares three optional features. `overwrite` includes `bindgen`, `direct-syscall` includes `sc`, and `io_safety` has no dependency expansion shown in the manifest. The README does not explain the semantics of these features, so they should not be enabled based on assumptions about performance or safety.
The `direct-syscall` entry does reveal an implementation choice. The normal dependency list includes `libc`, while `sc` is optional and included by `direct-syscall`. This suggests that applications can select an alternate syscall mechanism through Cargo features. The repository does not provide a tutorial explaining the differences, so an engineer should inspect the relevant implementation and test it on the target Linux system before adopting that configuration.
The repository is not archived, and its last push was on 2026-09-07. The repository has no GitHub releases, and the README does not publish a release cadence or migration policy. Engineers therefore cannot use this repository information to infer a regular upgrade schedule. The visible workspace contains the main crate, `examples`, `io-uring-test`, and `io-uring-bench`, while the manifest declares Rust edition 2021 and `rust-version = "1.63"`.
The package is licensed as `MIT OR Apache-2.0`, with repository files for both licenses. The contribution section also states that contributions are dual licensed under those options unless explicitly stated otherwise. This identifies the licensing model but does not replace review of the license texts for a particular distribution.
When io-uring Differs from a Direct Rust Read
A conventional `std::fs` read is the clearest alternative visible in the README example itself. A direct synchronous read passes through Rust's filesystem interface and returns its result without requiring the application to construct an io_uring entry, push it into a submission queue, wait for a completion, and correlate `user_data`.
tokio-rs/io-uring instead exposes the full submission protocol. It is useful when an application deliberately wants that protocol, whether to arrange multiple operations through a ring, associate completion data, or integrate with software built around io_uring. The repository's filesystem, network-programming, and asynchronous categories indicate the intended areas, but the documentation does not claim that the crate is faster than `std::fs` for a particular file size or workload.
The `examples/tcp_echo.rs` file shows that the repository includes a TCP-oriented example, but the README does not document its event loop, concurrency model, or completion strategy. It would be premature to describe it as a reusable async networking layer. The choice between io-uring and a direct Rust read should therefore be tied to queue-management requirements and verified operation-by-operation, not to a general performance assumption.
Editorial conclusion
Adopt tokio-rs/io-uring when a Linux Rust application needs direct io_uring access and can manage descriptor and buffer lifetimes through its unsafe API. Do not choose it for a simple file read without a specific need for queue-based I/O, and do not assume every opcode works on older kernels. Before integration, verify the required operation against the oldest supported Linux kernel, then confirm that the chosen Cargo features and syscall path work in that environment.
Frequently asked questions
Which Linux kernel version supports the tokio-rs/io-uring read example?
The README states that opcode `Read` is available after Linux kernel 5.6. It also says the example fails on an older kernel.
How do I add tokio-rs/io-uring to a Rust project?
Add `io-uring = "0.7"` under `[dependencies]` in `Cargo.toml`, as shown in the README. The repository manifest identifies the package as version 0.7.15.
What resources must remain valid during a tokio-rs/io-uring read?
The README says the submitted entry must remain valid, including its file descriptor and buffer. The buffer's storage therefore cannot be reallocated or dropped before the read completion has been processed.
Does tokio-rs/io-uring include a high-level async runtime?
The repository describes `io-uring` as a low-level userspace interface and documents queue submission and completion handling. It does not identify the crate as a complete async runtime.
Which license options apply to tokio-rs/io-uring?
The package metadata uses `MIT OR Apache-2.0`, and the repository includes `LICENSE-MIT` and `LICENSE-APACHE`. The README says contributions are dual licensed under these options unless explicitly stated otherwise.
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/tokio-rs-io-uring)Was this useful?