Async I/O on Linux with io_uring: A Practical Overview

This article covers Async I/O on Linux with io_uring: A Practical Overview. io_uring moves beyond readiness: submit I/O and receive completions. Learn the core concepts, when it helps, and minimal examples in C, Zig, and Rust.

Linux io_uring is an asynchronous I/O interface designed to reduce syscall overhead and improve throughput and latency under load.

Where epoll tells you “a fd is ready”, io_uring is closer to:

  • submit operations (read/write/open/stat, etc.)
  • receive completions when done

1) Key components

  • SQ (Submission Queue): you enqueue requests.
  • CQ (Completion Queue): kernel posts results.
  • SQE: submission queue entry.
  • CQE: completion queue entry.

Many operations can be submitted in batches, reducing syscalls.

2) When io_uring helps

  • high-throughput servers (lots of concurrent I/O)
  • workloads where syscall overhead is measurable
  • mixed operations: accept + read + write + fs ops

When it may not:

  • simple programs doing a little I/O
  • workloads dominated by CPU, not I/O

3) Minimal conceptual example

Real io_uring code is longer (setup, rings, feature flags). In production, you usually use a library.

Below are minimal “use a library” examples.

4) C: using liburing (conceptual snippet)

#include <liburing.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char **argv) {
    if (argc != 2) return 2;

    int fd = open(argv[1], O_RDONLY);
    if (fd < 0) return 1;

    struct io_uring ring;
    if (io_uring_queue_init(8, &ring, 0) != 0) return 1;

    char buf[4096];
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    io_uring_prep_read(sqe, fd, buf, sizeof(buf), 0);
    io_uring_submit(&ring);

    struct io_uring_cqe *cqe;
    io_uring_wait_cqe(&ring, &cqe);

    int res = cqe->res;
    io_uring_cqe_seen(&ring, cqe);

    if (res > 0) write(1, buf, (size_t)res);

    io_uring_queue_exit(&ring);
    close(fd);
    return 0;
}

5) Zig: calling liburing

Zig can link against C libraries and call them via @cImport.

const std = @import("std");

const c = @cImport({
    @cInclude("liburing.h");
    @cInclude("fcntl.h");
    @cInclude("unistd.h");
});

pub fn main() !void {
    var args_it = std.process.args();
    _ = args_it.next();
    const path = args_it.next() orelse return error.InvalidArgs;

    const fd = c.open(path, c.O_RDONLY);
    if (fd < 0) return error.OpenFailed;
    defer _ = c.close(fd);

    var ring: c.struct_io_uring = undefined;
    if (c.io_uring_queue_init(8, &ring, 0) != 0) return error.InitFailed;
    defer c.io_uring_queue_exit(&ring);

    var buf: [4096]u8 = undefined;
    const sqe = c.io_uring_get_sqe(&ring);
    c.io_uring_prep_read(sqe, fd, &buf, buf.len, 0);
    _ = c.io_uring_submit(&ring);

    var cqe: ?*c.struct_io_uring_cqe = null;
    _ = c.io_uring_wait_cqe(&ring, &cqe);
    const res = cqe.?.res;
    c.io_uring_cqe_seen(&ring, cqe.?);

    if (res > 0) {
        _ = c.write(1, &buf, @as(usize, @intCast(res)));
    }
}

6) Rust: use io-uring crate (high level)

// Cargo.toml: io-uring = "0.6"
use io_uring::{opcode, types, IoUring};
use std::fs::File;
use std::os::unix::io::AsRawFd;

fn main() -> std::io::Result<()> {
    let path = std::env::args().nth(1).expect("path");
    let f = File::open(path)?;

    let mut ring = IoUring::new(8)?;
    let mut buf = vec![0u8; 4096];

    let read_e = opcode::Read::new(types::Fd(f.as_raw_fd()), buf.as_mut_ptr(), buf.len() as _)
        .offset(0)
        .build()
        .user_data(1);

    unsafe {
        ring.submission().push(&read_e).expect("queue full");
    }

    ring.submit_and_wait(1)?;

    let cqe = ring.completion().next().unwrap();
    let res = cqe.result();
    if res > 0 {
        let n = res as usize;
        std::io::Write::write_all(&mut std::io::stdout(), &buf[..n])?;
    }

    Ok(())
}

7) Safety and operational notes

  • io_uring has a long tail of feature flags and kernel-version differences.
  • For production, you’ll want:
    • fixed buffers / registered files
    • careful backpressure
    • robust error handling

References