io_uring_sqpoll(7)
β¨ NAME β¨
io_uring_sqpoll β the whole overview of io_uring submission queue polling, bestie. Yes it's a mouthful, iykyk π
π¦ DESCRIPTION π¦
okay so. Submission queue polling (everyone calls it SQPOLL, say it with me) is a whole mode of operation. Instead of you poking the kernel every single time you have I/O to submit, an io_uring thread gets created in userspace-that-actually-lives-in-the-kernel, and it justβ¦ sits there. Monitoring the submission queue. Submitting requests on behalf of your application. π
That means your app doesn't have to make system calls to submit I/O at all β which drops latency and cuts CPU overhead for high-throughput workloads. It's giving effortless. β¨
π Why use SQPOLL?
In normal io_uring life, applications have to call io_uring_enter(2) (usually through io_uring_submit(3)) to tell the kernel "hey bestie, new submissions over here." Efficient, sure, but it still costs you system call overhead every time.
Turn SQPOLL on and the kernel thread is literally continuously polling the submission queue for new entries. The millisecond your application writes an SQE to the ring, the kernel thread snatches it up and submits it. The way that justβ¦ works. π«Ά
This provides:
- Elimination of submission system call overhead
- Lower and more predictable latency
- Better CPU utilization for high-IOPS workloads
And SQPOLL is most beneficial for:
- High-throughput storage workloads (NVMe, etc.)
- Latency-sensitive applications
- Workloads with continuous I/O streams
- Applications already running at high CPU utilization
π When SQPOLL may not help
not me being honest about this β SQPOLL is not universally beneficial, and each use case should be benchmarked to determine if it provides value. Situations where SQPOLL may not help or may actively hurt performance:
- Low-IOPS workloads: If the application submits I/O infrequently, the system call overhead being saved is negligible, and the polling thread wastes CPU cycles. π
- CPU-constrained systems: The polling thread consumes CPU. If the system is already CPU-bound, adding a polling thread may compete with the application for CPU resources, reducing overall performance.
- Bursty workloads: If I/O comes in bursts with idle periods, the polling thread may frequently sleep and wake, adding latency when it needs to wake up. Regular submission may be more efficient.
- Single-threaded applications on single-CPU systems: The polling thread and application will compete for the same CPU, potentially causing context switches that negate any benefits.
- Workloads dominated by completion handling: SQPOLL only optimizes submissions. If the application spends most of its time processing completions, SQPOLL provides little benefit.
Always benchmark with and without SQPOLL under realistic conditions. The performance difference can vary significantly based on hardware, kernel version, and workload characteristics.
fr fr. benchmark or perish. π
πΈ Enabling SQPOLL
You enable SQPOLL by setting the IORING_SETUP_SQPOLL flag when you create the ring. iconic:
struct io_uring ring;
struct io_uring_params params = {
.flags = IORING_SETUP_SQPOLL,
.sq_thread_idle = 2000, /* 2 seconds */
};
ret = io_uring_queue_init_params(entries, &ring, ¶ms);
The sq_thread_idle field is the vibe check on how long β in milliseconds β the kernel thread will poll before going to sleep if no submissions are pending. A value of 0 means the thread never sleeps. Uses more CPU but gives you the lowest latency. main character energy tbh. π₯
π§ The polling thread lifecycle
When the ring is created with SQPOLL, a kernel thread is spawned to service it. The thread's behavior is, in order:
- Poll the submission queue for new entries
- Submit any new requests found
- If no new entries are found for
sq_thread_idlemilliseconds, go to sleep - Wake up when signaled by the application
Your application can check if the thread is sleeping by examining sq->kflags for the IORING_SQ_NEED_WAKEUP flag using io_uring_sq_ready(3). If it's set, you must call io_uring_enter(2) with IORING_ENTER_SQ_WAKEUP to wake the thread:
/* After adding SQEs */
io_uring_smp_store_release(ring->sq.ktail, tail);
if (IO_URING_READ_ONCE(*ring->sq.kflags) & IORING_SQ_NEED_WAKEUP)
io_uring_enter(ring->ring_fd, 0, 0, IORING_ENTER_SQ_WAKEUP, NULL);
Good news bestie: io_uring_submit(3) handles this automatically, so you don't have to. π
π CPU affinity
By default, the kernel schedules the polling thread on any available CPU. But for better cache locality and reduced latency, you can pin the thread to a specific CPU:
struct io_uring_params params = {
.flags = IORING_SETUP_SQPOLL | IORING_SETUP_SQ_AFF,
.sq_thread_cpu = 3, /* pin to CPU 3 */
.sq_thread_idle = 1000,
};
The IORING_SETUP_SQ_AFF flag enables CPU affinity, and sq_thread_cpu says which CPU to use. Match made in heaven. π
π Credential requirements
Creating an SQPOLL ring traditionally required elevated privileges, because the kernel thread runs on behalf of the application. The requirements have evolved though, and the lore goes like this:
- Kernel 5.11 and earlier: requires
CAP_SYS_ADMINorCAP_SYS_NICE - Kernel 5.12 and later: unprivileged users can create SQPOLL rings, but the polling thread runs with reduced capabilities
- The
IORING_SETUP_NO_SQARRAYflag (kernel 6.6+) can simplify setup for SQPOLL-only rings
π Sharing the polling thread
Multiple rings can share a single polling thread using IORING_SETUP_ATTACH_WQ. This reduces resource usage when an application uses multiple rings β no cap, it's just efficient:
/* Create first ring with SQPOLL */
struct io_uring_params p1 = { .flags = IORING_SETUP_SQPOLL };
io_uring_queue_init_params(entries, &ring1, &p1);
/* Create second ring, attach to first ring's thread */
struct io_uring_params p2 = {
.flags = IORING_SETUP_SQPOLL | IORING_SETUP_ATTACH_WQ,
.wq_fd = ring1.ring_fd,
};
io_uring_queue_init_params(entries, &ring2, &p2);
π Completion handling
Read this part carefully okay β SQPOLL only affects submissions. Completions are still handled normally, which means the application must either:
- Poll the completion queue directly (busy-wait)
- Use
io_uring_enter(2)withIORING_ENTER_GETEVENTSto wait for completions - Use an eventfd for notification
For full polling on both submission and completion, combine SQPOLL with completion queue polling using io_uring_peek_cqe(3) or similar functions. Then you've ate. π
β Performance considerations
- CPU usage: The polling thread consumes CPU while active. If I/O is sporadic, the thread may waste cycles polling an empty queue. Set
sq_thread_idleappropriately for your workload. - Idle timeout tradeoff: A shorter idle timeout saves CPU but may increase latency when the thread needs to wake up. A longer timeout (or
0for never sleeping) uses more CPU but provides consistent low latency. - Batching: Even with SQPOLL, batching submissions by adding multiple SQEs before updating the tail pointer can improve throughput.
- CPU affinity: Pinning the polling thread to a CPU near the application's CPU can improve cache behavior and reduce cross-CPU communication.
π NOTES π
- The polling thread is per-ring (unless shared via
IORING_SETUP_ATTACH_WQ). Creating many SQPOLL rings without sharing can consume significant kernel resources. - SQPOLL rings still require system calls for:
- Waiting for completions (unless busy-polling the CQ)
- Waking the thread when it has gone idle
- Registration operations
- The polling thread inherits resource limits and cgroup membership from the creating process.
- If the polling thread encounters an error it cannot recover from,
IORING_SQ_CQ_OVERFLOWmay be set insq->kflags. - SQPOLL works well in combination with registered files and buffers, which further reduce per-I/O overhead.
π SEE ALSO π
io_uring(7), io_uring_setup(2), io_uring_enter(2), io_uring_queue_init_params(3), io_uring_register_files(3), io_uring_registered_buffers(7)
and that's the whole page bestie, mwah π this one's from Linux, dated January 18, 2025, and it lives in the manual as io_uring_sqpoll(7) β go forth and poll responsibly π¦β¨
π¦ where this page comes from
- alpine 3.24: liburing-doc 2.14-r0 Β· LGPL-2.1-or-later OR MIT
- arch rolling: liburing 2.15-1 Β· GPL-2.0-only WITH Linux-syscall-note OR MIT AND LGPL-2.0-or-later AND MIT
- nixpkgs unstable: liburing 2.14
- ubuntu resolute: liburing-dev 2.14-1
- ubuntu stonking: liburing-dev 2.15-1
- π the page's license: LGPL-2.0-or-later
- Copyright (C) 2025 Jens Axboe <axboe@kernel.dk>
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms