io_uring is Linux’s completion-oriented asynchronous I/O interface. The application places operation descriptions in a submission queue, the kernel performs those operations, and the application later reads completion events.

Prerequisites

Read IO Selectors and epoll first. OS Page Cache helps with the difference between cached file reads and storage-device reads.

The application and kernel communicate through shared submission and completion rings. user_data is copied from the submission queue entry to the completion queue event so the application can find the request state that should resume.

The Problem

epoll scales network readiness well, but it leaves the application to perform the operation after readiness is reported. That boundary is awkward for file I/O. A static file server can avoid blocking on socket reads and writes with epoll, yet still block when it opens or reads a file from disk.

io_uring moves the boundary. The application submits an operation such as “read from this file descriptor into this buffer” or “send this buffer on this socket.” The completion event reports the result after the kernel has tried the operation.

The Ring

An io_uring instance has two main queues:

QueueProducerConsumerContains
Submission queue (SQ)ApplicationKernelSubmission queue entries (SQEs): operation code, file descriptor, buffer pointer, length, offset, flags, and user_data.
Completion queue (CQ)KernelApplicationCompletion queue events (CQEs): result code, flags, and the original user_data.

The raw system calls are:

System callRole
io_uring_setupCreates the ring and shared queue metadata.
io_uring_enterSubmits queued SQEs and can also wait for completions.
io_uring_registerRegisters optional resources such as buffers or files to reduce per-operation overhead.

Most C programs use liburing, a helper library that wraps the raw ring setup, SQE preparation, submission, and CQE iteration.

Submission And Completion

The core loop is:

  1. Get an SQE.
  2. Fill it with an operation, such as recv, send, read, write, accept, or close.
  3. Store request identity in user_data.
  4. Submit one or more SQEs to the kernel.
  5. Read CQEs from the completion queue.
  6. Use each CQE’s user_data to resume the matching connection state.
  7. Inspect res: non-negative values are operation results, and negative values are -errno.

Completion order is not guaranteed to match submission order. If a program submits file read A and socket send B, B may complete first. That is why user_data matters: it lets the application attach a connection pointer, request ID, callback context, or index into a state table.

Queue Depth And Backpressure

The ring has finite capacity. Queue depth is the number of operations the application can have staged or in flight before it must wait, submit, reap completions, or fall back to an internal pending list.

If io_uring_get_sqe() returns no SQE, the application has filled the available submission side. The event loop should not blindly allocate more request state and assume the kernel will accept it. It needs backpressure:

StrategyEffect
Submit current SQEs and try againFrees space if the kernel consumes submissions.
Reap completionsFrees application-owned request state and may allow more work to be scheduled.
Keep a pending queuePreserves fairness when work arrives faster than the ring can accept it.
Stop reading from clients temporarilyPushes back on input instead of growing memory without bound.

The important mental model is that io_uring is not an infinite async task queue. It is a bounded kernel interface, so the application still owns overload behavior.

Multishot Operations

A normal SQE produces one CQE. Multishot operations submit one request that can produce multiple CQEs over time.

With multishot accept, the application submits one accept request, and the kernel can post a completion each time a new client connection is accepted. The IORING_CQE_F_MORE flag means the multishot operation is still armed and may produce more completions. When the flag is absent, the application should treat that multishot request as finished and decide whether to submit another one.

Multishot operations reduce resubmission overhead, but they make buffer and lifetime rules sharper. If one submitted request can produce many completions, the state attached to that request must live until the final completion.

Why It Can Be Faster

io_uring can reduce overhead in two ways:

MechanismWhy it helps
Batching submissions and completionsOne io_uring_enter can submit multiple operations and wait for completions, reducing syscall count.
Kernel-managed file I/OThe application thread can continue handling other completions instead of blocking in a file read.

The benefit depends on workload. If concurrency is low, files are hot in the page cache, and the application cannot batch operations, io_uring may add complexity without much speedup. If many clients and disk operations are active at once, the completion model can keep the application thread out of long blocking calls.

See also

Sources

  • Linux man-pages: io_uring(7), io_uring_setup(2), and io_uring_prep_multishot_accept(3).