Why io_uring Is Quietly Retiring epoll for Linux Async I/O
A developer recounts how building an educational reverse proxy called TinyGate pushed his team through three architectures in pursuit of nginx-class throughput: a naive worker-based version, an epoll rewrite, and finally a from-scratch io_uring implementation. The journey frames a practical comparison of Linux’s two async I/O models and why the newer one increasingly wins.
The core distinction is readiness versus completion. epoll, in the kernel since 2002, only tells an application when I/O is possible — the program must still issue its own read() or write() afterward, costing two syscalls per event plus the one-time registration. Each syscall triggers a user/kernel context switch, and that overhead compounds badly under high connection counts. io_uring, added in 2019 (kernel 5.1+), instead reports when I/O has actually completed. Submissions and completions live in shared ring buffers, so a single io_uring_enter() call can batch many operations and reap many results at once. With IORING_SETUP_SQPOLL, a dedicated kernel thread polls the submission queue, driving steady-state syscalls close to zero — though that thread burns CPU even when idle.
The author walks through minimal C examples (io_uring via liburing) showing epoll’s three-syscall pattern collapse into a leaner flow with no separate registration or read step. He also flags io_uring’s sharper edges: registered buffers and IORING_OP_SEND_ZC for true zero-copy, the CPU cost of SQPOLL, and asynchronous error handling via the completion entry’s res field rather than a normal return value. His takeaway is blunt — for any new project on a modern kernel, io_uring is the default, and clinging to epoll (or to kernels more than seven years old) is hard to justify.
Read the full article
Continue reading at Hacker News →This is an AI-generated summary. Read the original for the full story.