A normal io_uring request has a simple lifetime: userspace submits one SQE and eventually receives one CQE. Multishot operations change that relationship. One submitted request can remain active in the kernel and produce several completion queue entries as matching events occur.
That persistence changes completion handling from a one-CQE-per-request assumption into an explicit lifecycle protocol. The decisive state is carried by IORING_CQE_F_MORE: when the flag is present, the originating request can produce another completion; when it is absent, that multishot request has terminated.
One submission can represent a continuing operation
The submission and completion queues normally form a many-request stream:
SQE A ──► CQE A
SQE B ──► CQE B
SQE C ──► CQE CA multishot request changes the cardinality:
┌── CQE 1 + IORING_CQE_F_MORE
one multishot SQE├── CQE 2 + IORING_CQE_F_MORE
├── CQE 3 + IORING_CQE_F_MORE
└── CQE 4The final CQE lacks IORING_CQE_F_MORE. That absence is the lifecycle boundary. Userspace must not infer persistence merely from a successful result or from the operation type.
This model is useful for operations whose event source naturally repeats. Multishot accept can report multiple incoming connections. Multishot receive can report multiple arrivals. Multishot poll can continue reporting matching readiness events. The exact setup and constraints differ by opcode, but the completion-lifetime rule is shared.
IORING_CQE_F_MORE is request state, not payload state
A CQE contains both an operation result and flags. For multishot requests, those fields answer different questions.
cqe->res describes the result represented by that completion. For multishot accept, a successful result can contain the accepted file descriptor. For receive operations it can represent transferred bytes. For poll it represents the event mask.
cqe->flags & IORING_CQE_F_MORE describes whether more CQEs may arrive from the same submitted request.
A completion loop therefore has to process both dimensions:
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_request_error(cqe->res);
else
handle_result(cqe);
if (!active)
mark_request_inactive(cqe->user_data);
io_uring_cqe_seen(&ring, cqe);
}The code is schematic, but the separation is important. Result processing and request-lifetime tracking are related without being interchangeable.
Multishot accept removes repeated accept submissions
A conventional asynchronous accept flow must arrange another accept request after the previous one completes. A multishot accept keeps the accept operation armed across incoming connections.
With liburing, the setup can be compact:
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;Each accepted connection can produce another CQE. For io_uring_prep_multishot_accept(), a successful cqe->res contains the installed file descriptor. The application still owns the resource-management work associated with those accepted descriptors.
The persistent request removes repeated SQE preparation for the accept operation itself. It does not remove completion processing, connection admission policy, descriptor cleanup, or the need to detect termination of the multishot request.
Termination requires a new submission if service must continue
A multishot request is persistent, not permanent. Errors, explicit cancellation, or operation-specific termination conditions can end it. The final CQE does not carry IORING_CQE_F_MORE.
That means a server cannot treat the original SQE as an immortal registration:
CQE with MORE
│
├── process event
└── keep request state active
CQE without MORE
│
├── process final result
├── mark request inactive
└── submit replacement if policy requires continued serviceThis boundary also matters during cancellation. Canceling a multishot operation ends the continuing request; the application must account for the cancellation CQE separately from the terminal completion belonging to the multishot operation.
user_data is commonly used to associate those completions with application state. Since several CQEs can carry the same request identity over time, that state cannot be released after the first successful completion.
Receive variants add buffer-lifetime constraints
Multishot receive operations couple persistent request state with buffer selection. A multishot receive can consume buffers from a provided buffer group, and each completion can identify the selected buffer through CQE flags.
This creates two independent lifecycles:
request lifecycle:
submit ──► CQE ──► CQE ──► terminal CQE
buffer lifecycle:
available ──► selected ──► application use ──► returned to poolKeeping the request active does not make a consumed buffer reusable. Userspace must return buffers according to the provided-buffer mechanism before those buffers can serve later receives.
Buffer exhaustion can terminate a multishot receive. A design that tracks only socket readiness while ignoring buffer replenishment can therefore lose the persistence it expected from the original request.
Completion pressure moves to the CQ
Reducing repeated submissions does not reduce the number of events that the application must observe. A busy multishot source can generate CQEs rapidly, so completion-queue capacity and draining behavior remain part of the design.
This is a different pressure point from SQE submission overhead:
oneshot:
event → submit SQE → CQE → submit SQE → CQE
multishot:
submit SQE once → CQE → CQE → CQE → CQE
↑ completion pressure remainsApplications still need enough CQ capacity and timely completion consumption for their workload. Multishot changes request re-arming behavior; it does not turn repeated external events into one completion.
Persistent kernel state changes ownership assumptions
A one-shot request makes it tempting to bind application state to one expected completion. Multishot requests require a longer ownership interval. State referenced through user_data, cancellation bookkeeping, and operation-specific resources can remain relevant across many CQEs.
The safe lifecycle is tied to the terminal completion rather than the first completion:
allocate request state
│
submit multishot SQE
│
├── CQE + MORE
├── CQE + MORE
├── CQE + MORE
└── terminal CQE
│
release or re-arm stateThat distinction is the central boundary introduced by multishot I/O. The submission is singular, the completions are plural, and IORING_CQE_F_MORE tells userspace when the kernel-side sequence has reached its end.