A normal accept request has a one-to-one shape: one submitted operation eventually yields one completion. io_uring multishot accept changes that relationship. A single accept SQE can remain active across multiple incoming connections and emit a separate completion queue entry for each accepted socket.
The request is persistent, but not permanent. Each CQE carries enough state for the application to tell whether the original request can produce another completion. That boundary matters because a server that treats every successful CQE as proof that accept is still armed can silently stop accepting after the multishot request terminates.
One SQE can produce many CQEs
io_uring_prep_multishot_accept() prepares an accept operation that can complete repeatedly. For each accepted connection, the CQE result contains the newly installed file descriptor.
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_multishot_accept(
sqe,
listen_fd,
NULL,
NULL,
0
);
sqe->user_data = ACCEPT_TOKEN;The queue relationship differs from ordinary one-shot work:
submission queue completion queue
multishot accept SQE -------> fd 41, MORE
\----> fd 42, MORE
\---> fd 43, MORE
\--> final CQE, no MOREThis is not a batch of connections captured at submission time. The kernel keeps the accept request active and posts completions as connection requests arrive, until the operation reaches a terminating condition.
The persistence reduces the need to prepare and submit a replacement accept SQE after every successful connection. It also changes lifecycle accounting: counting submissions is no longer equivalent to counting completions.
IORING_CQE_F_MORE is the lifecycle signal
The flags field of each struct io_uring_cqe carries IORING_CQE_F_MORE while the multishot request remains capable of producing further CQEs. A completion without that flag marks the end of that request.
struct io_uring_cqe *cqe;
while (io_uring_peek_cqe(&ring, &cqe) == 0) {
bool active = cqe->flags & IORING_CQE_F_MORE;
if (cqe->res >= 0)
handle_connection(cqe->res);
else
handle_accept_error(cqe->res);
if (!active)
arm_multishot_accept_again();
io_uring_cqe_seen(&ring, cqe);
}The flag must be checked independently of a successful result. The useful invariant is not “a connection was accepted, therefore the request remains armed.” It is “the CQE says more completions can follow.”
This distinction also prevents a bookkeeping error around the final CQE. Once a CQE arrives without IORING_CQE_F_MORE, the original SQE is finished. A server that still needs an accept operation has to submit another one.
Completion identity belongs to the request
All completions generated by one multishot request retain the request identity supplied through user_data. The accepted file descriptor is returned in cqe->res; user_data still identifies the logical accept request.
That separation is useful in rings carrying mixed work:
user_data = ACCEPT_TOKEN
cqe->res = 41
flags = MORE
user_data = READ_TOKEN_FOR_CONN_7
cqe->res = 2048
flags = ...
user_data = ACCEPT_TOKEN
cqe->res = 42
flags = MOREA dispatcher can route CQEs by user_data, then interpret res according to the operation type. For multishot accept, one request identity can appear many times. Code that assumes user_data is consumed after its first CQE is incompatible with that model.
The same issue affects request-owned memory. State associated with the accept request cannot be released after the first successful completion if later CQEs still refer to that logical operation.
Errors terminate the active series
Multishot persistence does not convert every accept failure into a recoverable event inside the same request. When the operation terminates, its final CQE arrives without IORING_CQE_F_MORE. Error results are reported directly as negative errno values in cqe->res, following io_uring completion conventions.
The application therefore has two separate decisions:
cqe->res
|
+-- >= 0 -> accepted descriptor is available
|
+-- < 0 -> operation reported an error
cqe->flags
|
+-- MORE -> original request remains active
|
+-- no MORE -> original request is finishedTreating those fields separately produces a cleaner state machine. res describes this completion; IORING_CQE_F_MORE describes whether the originating multishot request continues.
Cancellation follows the same lifecycle boundary. A multishot request can be targeted by io_uring cancellation mechanisms. Once terminated, the accept series no longer produces further connection CQEs and must be explicitly armed again if the listener should continue accepting.
Direct accept changes descriptor placement
io_uring also provides multishot accept variants that place accepted sockets into the ring’s direct descriptor table. Direct descriptors are private to io_uring rather than entries in the process’s normal file descriptor table.
With io_uring_prep_multishot_accept_direct(), dynamically allocated direct descriptors can be returned through CQEs. This changes where the accepted socket is installed, but it does not remove the multishot lifecycle rule: the application still checks IORING_CQE_F_MORE to determine whether further completions can arrive from the request.
The distinction is architectural:
multishot accept
|
+-- regular variant -> process file descriptor table
|
+-- direct variant -> io_uring direct descriptor tableDirect descriptors can reduce interaction with a thread-shared descriptor table for workloads that keep subsequent I/O inside io_uring. They also impose a different resource-management model, including capacity in the registered file table. Descriptor placement and request persistence are separate choices.
CQ capacity becomes part of accept capacity
A persistent accept request can produce CQEs faster than the application consumes them. That makes completion-queue sizing and draining behavior part of the server’s admission path.
The multishot request removes repeated SQE submission from the steady-state accept loop, but it does not remove downstream work. Every accepted connection still creates a completion that must be consumed, a descriptor that must be owned or closed, and application state that may need allocation.
incoming connections
|
v
kernel accept path
|
v
CQEs accumulate
|
v
application drain rateIf the application stalls, pressure shifts toward the completion path and the resources associated with accepted sockets. A multishot interface changes submission frequency; it does not make connection handling unbounded.
This also makes shutdown ordering explicit. A server can stop admitting new work by canceling the multishot accept, consume the terminal completion, and then retire request-owned state. Closing or draining existing connections is a separate phase.
Persistent accept changes the control loop
With one-shot accept, the control loop commonly alternates between completion and resubmission. Multishot accept moves rearming out of the successful steady state and into the termination path.
one-shot:
submit -> accept CQE -> submit -> accept CQE -> submit
multishot:
submit -> accept CQE -> accept CQE -> accept CQE
|
v
terminal CQE
|
v
resubmitThat smaller submission loop is the main semantic shift. The application no longer owns rearming after every connection, but it still owns termination detection, CQ consumption, descriptor lifetime, cancellation, and recovery.
The reliable boundary is the final CQE without IORING_CQE_F_MORE. Everything before it belongs to one live request; everything after it requires a new submission.
References
- Linux
io_uring_multishot(7): https://man7.org/linux/man-pages/man7/io_uring_multishot.7.html - Linux
io_uring_prep_accept_direct(3): https://man7.org/linux/man-pages/man3/io_uring_prep_accept_direct.3.html - Linux
io_uring(7): https://man7.org/linux/man-pages/man7/io_uring.7.html