Photo by Taylor Vick on Unsplash
io_uring: Why Linux Finally Got Async I/O Right
For decades, writing high-performance I/O code on Linux meant navigating a fragmented landscape. Sockets got epoll. Files got POSIX AIO, which was famously awkward. High-throughput applications either spawned thread pools to fake async file I/O or tied themselves in knots managing multiple different async interfaces. io_uring, introduced in Linux 5.1, finally unifies all of this under a single, coherent model—and the performance implications are significant enough that nearly every major database and web server has either adopted it or is actively evaluating it.
The Problem with the Old Model
The POSIX I/O model is fundamentally synchronous. read() and write() block until the kernel is done. For network sockets, epoll offered a way to multiplex many connections without blocking, but it only tells you when a socket is ready to read—you still have to make the actual syscall after. For files, the situation was worse. POSIX AIO existed but came with so many restrictions (alignment requirements, poor cancellation, confusing error semantics) that most production systems simply used a dedicated thread pool to avoid it.
Every syscall also carries overhead: a context switch into the kernel, argument validation, and a switch back. For applications making thousands of small I/O operations per second, that overhead adds up fast. The goal with io_uring was to reduce syscalls to near zero while supporting both file and network I/O under one interface.
Two Ring Buffers, One Unified Interface
The core mechanism is elegant. io_uring sets up two ring buffers in memory shared between the application and the kernel: a Submission Queue (SQ) and a Completion Queue (CQ).
To perform I/O, your application writes a Submission Queue Entry (SQE) describing the operation—read this file descriptor, write this buffer—and advances a tail pointer. The kernel watches the queue, processes the operations, and writes results into the CQ. Your application polls the CQ for completions.
In the default mode, you still call io_uring_enter() to notify the kernel there’s work to do. But in kernel-polling mode (IORING_SETUP_SQPOLL), a kernel thread continuously monitors the SQ, eliminating even that syscall. For latency-sensitive applications doing sustained high-frequency I/O, this can reduce syscall overhead to essentially zero.
The other major win is batching. Where epoll requires one syscall per readiness notification and then another to act on it, io_uring lets you submit hundreds of operations in a single kernel entry. Linked operations—“write this buffer, then close this descriptor”—can be chained directly in the SQ.
Real-World Adoption
The uptake across the infrastructure world reflects genuine performance gains. PostgreSQL gained an io_uring backend to reduce the cost of its write-ahead log and buffer flushing. Storage engines like RocksDB and databases like ScyllaDB have explored or shipped io_uring support. Web servers and proxies are evaluating it for high-connection-count workloads where epoll’s per-event overhead becomes measurable.
The gains tend to be most pronounced in storage-heavy workloads—anything doing lots of random reads or buffered writes to disk—rather than purely network-bound workloads where epoll already works well.
The Security Tradeoff
io_uring has a significant security track record problem. Because it’s a large, complex kernel subsystem that can initiate arbitrary kernel operations on behalf of userspace, it has accumulated a notable number of privilege-escalation vulnerabilities. The shared memory interface, while great for performance, also creates new attack surface.
Several major cloud providers and container security platforms have restricted or outright disabled io_uring in sandboxed environments because the risk of container-escape exploits outweighed the performance benefits. Android disabled it for app processes. Google’s gVisor, the kernel-isolating sandbox used in their infrastructure, explicitly blocks it.
This tension—a kernel feature that delivers real performance gains but also opens a wide attack surface—is not unique to io_uring. eBPF has navigated similar scrutiny. The resolution typically follows the same pattern: heavy auditing over time, reduced default exposure, and selective enablement for trusted workloads.
Where This Fits
io_uring is part of a broader renegotiation of where the kernel boundary should sit for performance-critical software. eBPF pushed programmable logic into the kernel. DPDK bypasses the kernel entirely for networking. io_uring takes a middle path: keep the kernel in control but minimize the cost of interacting with it.
For systems programmers, the practical message is that io_uring is mature enough to use in production for trusted, non-sandboxed environments, and the liburing wrapper library makes it far more approachable than raw syscalls. For architects running workloads in shared or sandboxed infrastructure, the security constraints are real and should inform whether it’s appropriate—even when the benchmark numbers are compelling.