A Linux io_uring multishot accept request can produce several completion queue entries from one submission queue entry. The kernel keeps the accept operation active after a successful completion when the CQE carries IORING_CQE_F_MORE, so a server does not need to submit a fresh accept SQE for every connection.

This changes the lifetime contract between submission and completion. A normal oneshot request is finished after its CQE. A multishot accept can remain in flight across many accepted connections, and the CQE flags determine whether that request still exists.

One SQE can emit multiple CQEs

io_uring_prep_multishot_accept() prepares an accept operation whose successful activity can be reported repeatedly. Each accepted connection produces a CQE, with the installed file descriptor returned in cqe->res.

struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);

io_uring_prep_multishot_accept(
    sqe,
    listen_fd,
    NULL,
    NULL,
    SOCK_CLOEXEC
);
sqe->user_data = ACCEPT_TOKEN;

After submission, several incoming connections can generate completions associated with the same user_data.

SQE user_data = ACCEPT_TOKEN

CQE 1: res = 12, flags = IORING_CQE_F_MORE
CQE 2: res = 15, flags = IORING_CQE_F_MORE
CQE 3: res = 18, flags = IORING_CQE_F_MORE

The repeated CQEs do not represent repeated SQE submissions. They are separate results from one request that remains active.

IORING_CQE_F_MORE defines request lifetime

For a multishot request, IORING_CQE_F_MORE is the key lifetime signal. When it is present, more completions may follow from the same request. When a CQE arrives without that flag, the multishot request has terminated and no later CQE should be expected from it.

A completion loop therefore needs to treat the result and the lifetime flag as separate state.

struct io_uring_cqe *cqe;

io_uring_wait_cqe(&ring, &cqe);

int res = cqe->res;
bool active = cqe->flags & IORING_CQE_F_MORE;

if (res >= 0) {
    int client_fd = res;
    /* hand client_fd to connection state */
}

if (!active) {
    /* this multishot accept request is finished */
}

A successful accepted descriptor does not by itself prove that the request remains active. The flag carries that information.

Final completion creates a rearm boundary

Multishot removes per-connection resubmission while the request stays alive, but it does not make accept permanent. An error, explicit cancellation, or another terminating condition can end the request. The final CQE lacks IORING_CQE_F_MORE.

If the listener should continue accepting after termination, user space must submit another accept request. That creates a distinct operational boundary:

submit multishot accept
        |
        +-- CQE + MORE
        +-- CQE + MORE
        +-- final CQE
              |
              +-- request no longer active
              +-- submit replacement if service continues

Code that assumes the initial SQE remains armed forever can silently stop accepting connections after the terminal completion.

Cancellation terminates the persistent request

Because the request can outlive many individual completions, shutdown logic has to target the request itself. An IORING_OP_ASYNC_CANCEL operation can cancel a multishot request, commonly by matching its user_data.

After successful cancellation, the multishot operation produces a terminal completion without IORING_CQE_F_MORE; documented cancellation behavior reports -ECANCELED for that final result.

This makes request identity important beyond correlating one submission with one completion. The identity can represent a long-lived kernel operation that remains cancelable between connection arrivals.

Direct accept changes descriptor placement

io_uring_prep_multishot_accept_direct() combines multishot behavior with the io_uring fixed-file table. Instead of installing each accepted socket into the process file-descriptor table, the kernel can allocate an unused direct descriptor slot for each connection.

The fixed-file table must already have suitable space. With dynamic allocation, exhaustion is reported through the CQE result, such as -ENFILE when no free direct slot is available.

This variant changes where accepted sockets are installed, not the multishot lifetime rule. IORING_CQE_F_MORE still indicates whether the original accept request can generate further completions.

Completion capacity becomes part of backpressure

A single active accept request can generate CQEs as connections arrive. Removing repeated SQE submissions does not remove completion-side capacity constraints. The application still has to consume CQEs promptly and size the completion path for its traffic pattern.

The important accounting unit is no longer one SQE paired with exactly one CQE. A long-lived multishot SQE can have a one-to-many relationship with CQEs.

That property affects bookkeeping. Per-request state cannot be freed after the first successful completion if later completions still refer to the same request identity. Cleanup belongs at the terminal CQE, while each accepted descriptor gets its own connection lifetime.

Multishot changes submission economics, not accept semantics

Each successful result still represents a distinct accepted connection. Multishot primarily changes how the accept operation is armed and how completions are delivered: one SQE can stay resident and report multiple accepted sockets.

The interface therefore separates two lifetimes that oneshot code often treats as identical. Accepted sockets begin independent connection lifetimes, while the originating accept request may remain active across all of them. IORING_CQE_F_MORE is the boundary that keeps those lifetimes from being conflated.