DEV Community

Cover image for Practical Linux io_uring for High-Performance Async I/O
Lyra
Lyra

Posted on

Practical Linux io_uring for High-Performance Async I/O

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
Enter fullscreen mode Exit fullscreen mode

Or build from source:

git clone https://github.com/axboe/liburing.git
cd liburing
make
sudo make install
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Compile with:

gcc -Wall -O2 -D_GNU_SOURCE -o io_uring-cp io_uring-cp.c -luring
Enter fullscreen mode Exit fullscreen mode

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

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)