An order gateway uses epoll with 3,000 sockets and processes each ready event in 2 microseconds. Compare it with io_uring on the same workload, then state the case where epoll is the better choice for a trading system.
An order gateway uses epoll with 3,000 sockets and processes each ready event in 2 microseconds. Compare it with io_uring on the same workload, then state the case where epoll is the better choice for a trading system.
Approach: Count syscalls per unit of work in each model, then separate the throughput argument from the latency argument.
io_uring wins on syscalls and throughput, epoll wins when a single hot session needs the lowest possible latency and the process can afford to busy poll it. With epoll the loop costs one epoll_wait syscall for a batch of ready descriptors plus one read syscall per ready socket, so 3,000 active sockets cost 3,001 syscalls per pass at roughly 100 ns of raw entry and exit each, 300 microseconds of pure overhead before any work happens. io_uring uses two shared ring buffers, so the application writes submission entries into memory and completions appear in memory, with batching that lets many operations be submitted per syscall and with SQPOLL mode removing the syscall entirely by having a kernel thread poll the submission ring. That turns 3,001 syscalls into one or zero. The latency picture is different: for one socket that is nearly always ready, epoll plus a nonblocking read on a busy-polled core has a shorter and more predictable path than the ring handoff, and SQPOLL costs a dedicated kernel thread burning a core. So the gateway with thousands of sessions takes io_uring for aggregate cost, and the single hot market data or order path takes bypass or a tight epoll loop for the tail.
Follow-up: SQPOLL removes the submission syscall by spinning a kernel thread. How do you size its idle timeout, and what happens to latency when it sleeps?
Key concepts: epoll, io_uring, syscall, batching.