Practical Linux io_uring for High-Performance Async I/O
If you've ever benchmarked a high-throughput server or tool on Linux and hit the wall of traditional I/O models, io_uring is the kernel feature that changes the game. Introduced in Linux 5.1 and maturing rapidly, it gives userspace direct, low-overhead access to the kernel's I/O submission and completion queues.
Unlike epoll or classic async patterns that still require system calls per operation, io_uring lets you submit batches of reads, writes, accepts, and more with a single syscall — and get completions without constant polling overhead.
Why io_uring Matters in 2026
Traditional POSIX I/O (read/write) is synchronous and blocking by default. Moving to epoll + non-blocking still incurs per-operation syscall and context-switch costs. For workloads like web servers, log shippers, or data pipelines handling thousands of concurrent operations, that adds up.
io_uring solves this with two shared ring buffers between kernel and userspace:
- Submission Queue (SQ): where you post I/O requests
- Completion Queue (CQ): where the kernel posts results
The library that makes this ergonomic is liburing (maintained by Jens Axboe, the io_uring author).
Getting Started with liburing
On Debian/Ubuntu:
sudo apt update
sudo apt install liburing-dev
Or build from source:
git clone https://github.com/axboe/liburing.git
cd liburing
make
sudo make install
A minimal example that copies a file using io_uring (adapted from liburing examples):
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include "liburing.h"
#define QD 64
#define BS (32*1024)
int main(int argc, char *argv[]) {
struct io_uring ring;
int ret;
if (argc != 3) {
fprintf(stderr, "Usage: %s <infile> <outfile>\n", argv[0]);
return 1;
}
ret = io_uring_queue_init(QD, &ring, 0);
if (ret < 0) {
fprintf(stderr, "queue_init: %s\n", strerror(-ret));
return 1;
}
// ... (setup read/write requests using io_uring_prep_read/write)
// Submit with io_uring_submit()
// Wait completions with io_uring_peek_cqe / io_uring_cqe_seen
io_uring_queue_exit(&ring);
return 0;
}
Compile with:
gcc -Wall -O2 -D_GNU_SOURCE -o io_uring-cp io_uring-cp.c -luring
For full working examples including UDP servers and web servers, see the excellent io_uring-by-example series.
Production Considerations
- Kernel version: Target Linux 5.5+ for the richest feature set (linked SQEs, registered files, SQPOLL).
- Registered files: Dramatically reduces fd lookup overhead for hot paths.
- SQPOLL: Kernel thread polls the submission queue — great for very high rates but uses a CPU core.
- Error handling: Always check CQE res field; negative values are -errno.
- Memory ordering: Use READ_ONCE/WRITE_ONCE or proper barriers when accessing rings directly.
Real-World Use Cases
- High-performance proxies and load balancers (replacing epoll loops)
- Database storage engines and WAL writers
- Log aggregation agents pushing millions of events
- Custom async runtimes for language VMs
Many projects have adopted it: fio, RocksDB experiments, several Rust async runtimes, and high-frequency trading tooling.
Sources & Further Reading
- Official liburing: https://github.com/axboe/liburing
- io_uring paper by Jens Axboe: https://kernel.dk/io_uring.pdf
- LWN coverage and kernel docs
- io_uring-by-example repository (highly recommended for progressive examples)
io_uring isn't just another async API — it's the modern Linux way to do scalable I/O. Start with liburing's examples, measure your before/after latency and throughput, and you'll quickly see why it's becoming the default for performance-sensitive Linux workloads.
This article was written with hands-on verification against current liburing main branch and kernel 6.12+ behavior.
Top comments (0)