io_uring is real, useful, and in some workloads extremely fast. But it is not magic. It’s a Linux-specific asynchronous I/O interface built around shared ring buffers between user space and the kernel. And the truth is a lot more interesting than the sales pitch.
Key Takeaways
- io_uring uses two shared ring buffers: a submission queue (SQ) and a completion queue (CQ).
- It can reduce syscall overhead by batching many operations behind fewer
io_uring_enter()calls. - In some modes, especially submission queue polling, applications can avoid submission syscalls almost entirely, at the cost of extra CPU.
- io_uring provides a unified async API for files, sockets, and related operations, which is one reason developers like it.
- It is not automatically faster than
epoll,read,write, or other existing APIs in every case. - For network I/O, gains are often modest unless syscall overhead and context switching are real bottlenecks.
- Features like fixed buffers, fixed files, and zero-copy send support can cut per-operation overhead further.
- There are real security and operational concerns. io_uring’s power and kernel complexity mean you should be careful, especially on older kernels.
What io_uring Actually Is
At its core, io_uring is a Linux async I/O facility. The design is simple enough to describe without hand-waving.
You create:
- a submission queue (SQ) for requests
- a completion queue (CQ) for results
These queues live in memory shared between user space and the kernel. Your program fills submission queue entries, or SQEs, with operations like read, write, accept, or send. The kernel later posts CQEs, completion queue events, with the results.
According to the official io_uring(7) man page, the kernel places exactly one matching CQE for every SQE in the normal model, and errors come back as -errno values in the res field rather than through errno.
That design matters because it shifts the mental model from readiness-based I/O to completion-based I/O. With select, poll, or epoll, we ask, “is this fd ready?” With io_uring, we say, “do this work,” then collect completions later.
Why io_uring Performance Gets So Much Attention
The hype exists for a reason. io_uring can remove overhead in places where older APIs force extra trips into the kernel.
The main performance ideas are:
- Shared ring buffers reduce kernel-user coordination overhead
- Batching lets us submit multiple operations with one syscall
- SQ polling can remove submission syscalls entirely
- Fixed buffers and fixed files cut repeated setup costs
- Zero-copy support can avoid extra data movement in some network paths
The Unixism documentation cites benchmark figures from the original io_uring paper: around 1.7 million 4K IOPS in polling mode versus 608k for Linux AIO, and about 1.2 million IOPS without polling on the referenced machine. It also mentions a raw throughput test hitting 20 million messages per second with a no-op request type.
Those numbers are impressive. But they’re also very workload-specific. And that’s where the hype usually goes off the rails.
Where io_uring Helps in Real Systems
The Red Hat write-up makes a useful point that often gets lost: io_uring has been a big win for file I/O, while for network I/O the gains may be only modest because networking already has decent non-blocking APIs.
That lines up with what many of us see in practice. io_uring tends to help most when:
- the app does lots of small I/O operations
- syscall overhead is non-trivial
- the app can batch submissions well
- the workload benefits from a single async interface for both files and sockets
- reducing context switches actually matters
And sometimes the biggest win isn’t raw throughput. It’s architecture. Having one async model for file reads, writes, socket accepts, sends, receives, timeouts, and more can simplify certain event-driven systems.
Why io_uring Isn’t Automatically Faster Than epoll
This is the part worth saying plainly: io_uring vs epoll is not a universal victory for io_uring.
epoll is a readiness notification API. It tells you when a socket is ready. You still do the read() or write() calls yourself. io_uring can directly queue the operation and later hand you the result. That can be cleaner, and sometimes cheaper. But only if your workload benefits from that model.
For network servers, a naive io_uring implementation may still bounce through a lot of kernel work. Red Hat showed an echo server example where a simple design still needed thousands of io_uring_enter calls. In their strace output for 1,000 client requests, the server made 4,001 io_uring_enter syscalls. That’s not exactly “syscalls are gone.”
So if someone tells you io_uring eliminates syscalls, the accurate answer is: sometimes fewer, not always none.
io_uring and the Three Syscalls You Should Know
The Linux API revolves around three syscalls:
io_uring_setupio_uring_registerio_uring_enter
From Red Hat’s explanation:
io_uring_setupcreates the contextio_uring_registerlets you register files or user buffersio_uring_enterkicks off submission and/or waits for completions
Most application code uses liburing, which wraps the low-level details nicely.
A tiny example looks like this:
struct io_uring ring;
io_uring_queue_init(256, &ring, 0);
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe, fd, iov, 1, 0);
io_uring_submit(&ring);
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
if (cqe->res < 0) {
// error: -errno in res
} else {
// bytes read in cqe->res
}
io_uring_cqe_seen(&ring, cqe);If you’re new to Linux I/O internals, this pairs nicely with our post on how Linux actually schedules CPU time, because syscall reduction only matters in the context of scheduling and wakeups.
io_uring Features That Actually Matter
Fixed buffers and fixed files
LWN’s coverage of zero-copy network transmission highlights one of the less flashy but very practical features: fixed buffers and files.
These are resources that stay registered and ready inside the kernel, which avoids repeating setup and teardown work for every operation. For hot paths, that can be a real gain.
Submission queue polling
SQ polling lets a kernel thread watch the submission queue so your application doesn’t need to call io_uring_enter() every time. That can be fast. But it’s not free.
The tradeoff is simple:
- lower syscall overhead
- higher CPU usage, because a kernel thread is polling
This is classic systems engineering. We’re moving cost around, not deleting it.
Zero-copy network transmission
LWN also described io_uring’s zero-copy send path, including IORING_OP_SENDZC. The important nuance is that “send completed” does not necessarily mean the buffer is safe to reuse. Real completion may happen later, after the kernel and hardware are done with it.
That’s a subtle point, and it’s exactly why network I/O performance is rarely as simple as benchmark headlines suggest.
io_uring Security Concerns Are Part of the Story
If we want to understand io_uring without the marketing haze, we also need to mention security.
Paul Moore’s talk, “io_uring: So Fast. It’s Scary.”, called attention to how powerful this interface is and noted that Linux Security Module access controls were not added until Linux 5.16. That doesn’t make io_uring inherently bad. But it does mean kernel version and deployment context matter.
In other words:
- newer kernels matter
- feature exposure matters
- container and sandbox environments need extra thought
- “fast” should not override “safe”
If you care about kernel behavior more broadly, our article on Linux page cache explained is another useful companion read.
Best Practices for Using io_uring Without Fooling Yourself
Here’s my practical checklist:
- Benchmark your real workload, not a toy benchmark
- Start with liburing unless you truly need the low-level API
- Use batching well before chasing exotic flags
- Treat SQ polling as a measured tradeoff, not a default
- Consider fixed buffers/files for hot paths
- Be careful with ordering on stream sockets
The man page is explicit here: for TCP sockets, it is generally unsafe to have more than one outstanding send, or more than one outstanding receive, on a socket at a time unless you use the right ordering and pipeline features such as IOSQE_IO_LINK or newer bundle-style mechanisms.
And yes, read the docs. The authoritative starting point is the io_uring(7) manual page.
A Simple Way to Think About io_uring
If I had to explain io_uring in one sentence, I’d say this:
io_uring is a Linux async I/O interface that can reduce syscall and setup overhead by moving more of the conversation with the kernel into shared queues.
That’s it. Not magic. Not a silver bullet. Just a strong tool with real tradeoffs.
Conclusion
io_uring deserves attention, but not worship. It gives Linux developers a powerful completion-based I/O model, shared ring buffers, batching, polling modes, and advanced features like fixed buffers and zero-copy sends. In the right workload, that can translate into serious gains.
But the marketing version leaves out the important part: those gains are conditional. Network I/O may see only modest improvements. Polling burns CPU. Ordering rules matter. And security history matters too.
If you’re curious, try building a small file or socket workload with liburing, measure it honestly, and compare it to your current approach. And if you’ve already done that, leave a comment. I’m always interested in benchmarks that survived contact with production.
Sources
Red Hat Developer, Why you should use io_uring for network I/O
https://developers.redhat.com/articles/2023/04/12/why-you-should-use-iouring-network-ioMichael Kerrisk, man7.org, io_uring(7) — Linux manual page
https://man7.org/linux/man-pages/man7/io_uring.7.htmlUnixism, What is io_uring? — Lord of the io_uring documentation
https://unixism.net/loti/what_is_io_uring.htmlLWN.net, Zero-copy network transmission with io_uring
https://lwn.net/Articles/879724/Paul Moore, Microsoft, io_uring: So Fast. It’s Scary.
https://www.youtube.com/watch?v=AaaH6skUEI8