Subroutine Logo
Subroutine
← Back to Articles Systems Advanced 6 min read

Linux Event Loops & Async I/O: epoll vs io_uring

An interactive systems exploration of C10K/C1000K scalability, POSIX select/poll bottlenecks, epoll red-black trees, and Linux kernel io_uring ring buffers.

Published: 2026-07-28
#Systems#Linux#epoll#io_uring#Async I/O#C10K

Building web servers, high-concurrency proxies (like Nginx, HAProxy, and Envoy), or async runtimes (Node.js, Tokio, and libuv) requires handling tens of thousands of concurrent open socket connections (the C10K / C1000K problem).

Over the last 25 years, Linux async I/O has evolved through three distinct paradigms: select / poll, epoll, and io_uring.


1. Summary & Key Takeaways

  • POSIX select() / poll() Bottleneck: Scans the entire file descriptor array linearly (O(N)O(N)) on every loop iteration. With 10,000 open sockets, inspecting 5 active sockets forces 10,000 array checks in the kernel.
  • Linux epoll (O(1)O(1)): Uses an in-kernel Red-Black Tree to track open sockets and a Ready List populated by NIC interrupts. Calling epoll_wait() returns only active file descriptors in O(1)O(1) time.
  • Linux io_uring: Eliminates kernel context switch overhead entirely! Uses a pair of lock-free circular ring buffers - a Submission Queue (SQ) and a Completion Queue (CQ) - shared directly between user space and the kernel via mapped memory.
  • Zero-Syscall Mode (IORING_SETUP_SQPOLL): A kernel thread polls the Submission Queue directly, allowing user applications to submit read/write I/O requests with zero system calls.

2. Interactive Async I/O & Event Loop Benchmark

Simulate workload performance below with up to 10,000 concurrent sockets across POSIX select, Linux epoll, and io_uring!

Async I/O Experiment

Scale connection count to 10,000 sockets. Watch select flood the CPU with syscalls, epoll reduce overhead to active sockets, and io_uring process batch events with 0 system calls!

Linux Async I/O & Event Loop Benchmark

Compare O(N) select/poll vs O(1) epoll vs Zero-Syscall io_uring Ring Buffers

Submission Queue (SQ)

User Space writes I/O requests without syscalls

0 Entries
Completion Queue (CQ)

Kernel posts completed I/O results asynchronously

0 Completed
Processed Events0
Kernel Syscalls0 (Polled Ring)
Time ComplexityO(1) Ring

3. Kernel Architecture Comparison

graph TD
    subgraph Select["POSIX select / poll"]
    A1["App User Space"] -->|"1. Syscall 10k FDs"| K1["Kernel Array Scan O(N)"]
    K1 -->|"2. Copy Active List"| A1
    end

    subgraph Uring["Linux io_uring Ring"]
    U2["App User Space"] -->|"Push SQEs"| SQ["Submission Queue Ring"]
    SQ -->|"SQPOLL Kernel Thread"| CQ["Completion Queue Ring"]
    CQ -->|"Read CQEs (0 Syscalls)"| U2
    end

4. Async I/O Protocol Matrix

InterfaceTime ComplexityKernel Copy per LoopSyscalls per I/OMax Concurrency
select()O(N)O(N) LinearCopy full FD set1 per loop iteration1,024 (FD_SETSIZE)
poll()O(N)O(N) LinearCopy array of pollfd1 per loop iterationUnlimited (RAM bounded)
epoll()O(1)O(1) ConstantNone (Red-Black Tree)1 per batch (epoll_wait)Millions
io_uringO(1)O(1) ConstantZero (Shared Mapped Ring)Zero (SQPOLL mode)Millions