epoll vs io_uring: which Linux async I/O wins?
Key Takeaways
- io_uring reduces syscall overhead by batching operations through shared memory ring buffers
- epoll requires two syscalls per I/O event (epoll_wait + read/write), creating overhead at scale
- io_uring's SQPOLL mode can achieve near-zero syscalls during steady state
A developer and their students rebuilt a reverse proxy three times before landing on io_uring, Linux's modern async I/O interface. Their journey, documented in a post now circulating on Hacker News, illustrates why epoll, the 23-year-old workhorse, struggles at scale. The culprit: syscall overhead that io_uring sidesteps entirely.
The project, called TinyGate, started as a worker-based reverse proxy. It worked, but couldn't touch nginx or haproxy in benchmarks. Version two used epoll and saw dramatic gains. Version three switched to io_uring, requiring a complete rewrite. The performance difference justified the pain.
Why does epoll create syscall overhead?
epoll landed in the Linux kernel in 2002. It tells your application when I/O is possible, not when it's done. This distinction matters. When epoll_wait returns, you still need to call read() or write() yourself. That's two syscalls per I/O event, plus the initial epoll_ctl registration. Each syscall triggers a context switch between user and kernel mode.
At low connection counts, this overhead is negligible. At thousands of concurrent connections, it adds up fast. Every context switch burns CPU cycles and cache coherency.
How io_uring changes the model
io_uring arrived in kernel 5.1 (2019), seventeen years after epoll. Instead of a readiness model, it uses a completion model. The kernel tells you when I/O is done, not when it's possible. No polling loop. Far fewer syscalls.
The architecture uses two ring buffers in shared memory between your application and the kernel. You submit operations to the submission queue (SQ). The kernel posts results to the completion queue (CQ). Both live in memory both sides can access directly.
By default, you call io_uring_enter() to tell the kernel "check the submission queue." But one call can submit a batch of operations and reap a batch of completions. Compare that to epoll's one syscall pair per operation.
SQPOLL: near-zero syscalls at steady state
io_uring offers an aggressive optimization: IORING_SETUP_SQPOLL. This spins up a dedicated kernel thread that continuously polls the submission queue. Your application writes submissions to shared memory; the kernel thread picks them up without any syscall from your side.
The tradeoff is CPU cost. That kernel thread burns cycles polling. For I/O-heavy workloads where syscall overhead dominates, the math often works out. For lighter loads, the spinning thread wastes more than it saves.
| Feature | epoll | io_uring |
|---|---|---|
| Model | Readiness (notifies when I/O possible) | Completion (notifies when I/O done) |
| Syscalls per I/O | 2 (epoll_wait + read/write) | 1 per batch (or 0 with SQPOLL) |
| Memory sharing | None | Ring buffers in shared memory |
| Kernel version | 2.5.44 (2002) | 5.1 (2019) |
| Best for | Moderate connection counts | High-throughput, many connections |
What the code looks like
The original post includes C examples for both approaches. The epoll version creates an instance with epoll_create1(), registers a file descriptor with epoll_ctl(), then blocks on epoll_wait(). When it returns, you handle the event with standard read/write calls.
io_uring code (using liburing, the userspace helper library) sets up the ring once, then submits operations by writing to the submission queue. Completions arrive in the completion queue without additional syscalls per operation.
The architectural shift is significant. With epoll, your application orchestrates I/O timing. With io_uring, the kernel handles that work. You describe what you want done; the kernel tells you when it's finished.
When to use which
For systems running kernel 5.1 or newer, io_uring is usually the better choice for network servers handling many concurrent connections. The syscall savings compound as connection count rises.
epoll still makes sense in a few cases. Older kernels that predate io_uring. Applications where I/O isn't the bottleneck. Codebases where the rewrite cost outweighs the performance gain. Simple utilities that don't need the complexity.
The TinyGate team found the rewrite painful but worthwhile. Their benchmarks still trailed nginx and haproxy, both of which have decades of optimization. But the gap narrowed.
Logicity's Take
The real story here isn't just performance numbers. It's that Linux async I/O finally caught up to what application developers actually need: batching, shared memory, and completion notification. io_uring represents a philosophical shift from "the kernel tells you when to act" to "the kernel acts on your behalf." For anyone building high-throughput network services today, learning io_uring isn't optional. It's table stakes.
Frequently Asked Questions
What kernel version is required for io_uring?
Linux kernel 5.1 or newer, released in 2019. Feature completeness improved significantly in subsequent versions, so 5.10+ is recommended for production use.
Does io_uring replace epoll completely?
Not entirely. epoll remains useful for simpler applications, older kernels, and cases where I/O isn't the bottleneck. io_uring adds complexity that isn't always justified.
What is SQPOLL in io_uring?
SQPOLL (IORING_SETUP_SQPOLL) creates a dedicated kernel thread that polls the submission queue continuously. This eliminates syscalls during steady-state operation but consumes CPU cycles for the polling thread.
Why does epoll require two syscalls per I/O event?
epoll uses a readiness model. epoll_wait tells you I/O is possible, then you must call read() or write() to actually perform it. That's two syscalls, each requiring a user-kernel context switch.
Is io_uring harder to implement than epoll?
Yes. io_uring has a steeper learning curve and requires understanding ring buffer mechanics. Libraries like liburing simplify the interface, but the conceptual model is more complex than epoll's.
Need Help Implementing This?
Building high-performance network services with io_uring or optimizing existing epoll-based systems? Reach out to Logicity for technical consulting on Linux performance engineering and async I/O architecture.
Source: Hacker News: Best
Manaal Khan
Tech & Innovation Writer
Produced with AI assistance and reviewed by the Logicity editorial team. Learn more in our Editorial Policy.
Related Articles
More in Trending Tech
Humanity Just Went Farther Into Space Than Ever Before — And Made It Back Alive
Four astronauts splashed down in the Pacific Ocean on April 10, 2026, after traveling farther from Earth than any human beings in history. The Artemis II crew shattered a 56-year-old distance record set by Apollo 13, journeying nearly 253,000 miles from our planet during their 10-day lunar flyby mission. This marks the first time humans have ventured beyond low Earth orbit since 1972.

Amflow's Electric Bikes Are Blowing The Competition Away
Amflow, the e-bike brand spun out of DJI, has just released two impressive new electric mountain bikes that are breaking the mold with unprecedented power, range, and lightness. The flagship bikes are powered by the innovative Avinox motors and come with features like onboard navigation and heart rate control.

Canva Just Made a Power Play: Here's What It Means for the Future of Design and Marketing
Canva has made a bold move by acquiring two companies, Simtheory and Ortto, to boost its AI and marketing automation capabilities. This strategic move is set to revolutionize the way teams work on design and marketing projects. With these acquisitions, Canva is poised to become an all-in-one platform for businesses and individuals alike.


