Difference between epoll and io_uring

Understanding the difference between epoll and io_uring in Linux is crucial for modern backend developers.

io_uring represents a generational leap in Linux asynchronous I/O—eliminating the long-standing CPU and system call overheads that traditional networking frameworks relied on for decades.

When you deal with a web-based application, many things are running on the networking stack of Linux, for example

HTTP & REST APIs:

Every incoming request from a frontend or microservice needs an open socket connection.
• Database Queries: When your Node.js, Go, or Python backend fetches data from PostgreSQL, MongoDB, or Redis, it communicates over network sockets.
• Real-time Streaming: WebSockets, chat systems, gaming servers, and live video platforms keep thousands of concurrent connections alive.
• Internal Routing: Reverse proxies like Nginx, Envoy, and API Gateways handle millions of incoming packets every second.
If you are building a system where you are expecting a lot of traffic, then you must remember that your CPU spends more time waiting on Io
operations and networking operations.
But why does the CPU spend so much time waiting? Because networking and storage tasks run asynchronously, the CPU constantly spends cycles on context switching,

polling event loops, and copying memory buffers between kernel and user space.” We will see what is ontext switching and polling event loops later in this chapter

Before we begin to see the difference between epoll and io_uring, we need to understand their origins and their functionality, so that we can have a clear picture of how io_uring is best for you in several cases instead of epoll

History of io_uring (Revolution before revolution)

io_uring is one of the revolutionary concepts, and in many cases it is clearly seen as a successor of epoll, but you know? When epoll came in to existance, it was also seen as a revolution.

epoll in Linux is a system call and an I/O event notification facility.
Epoll is also a family of system calls; the following are system calls related to epoll (epoll_create, epoll_ctl, epoll_wait) that manage connections using a Watchlist
and a Ready List.
At its core, epoll is an I/O Event Notification Facility.
Because Linux treats “everything as a file” (sockets, files, pipes, timers), epoll simply acts as a smart alarm system—it monitors a list of File Descriptors and alerts your application whenever one of them is ready to read or write. (For example: as we said, read and write; if data comes on a socket, it alarms that a read operation has arrived)
Why is it mostly associated with Networking? Even though it works on various I/O types, ~90% of its real-world usage is in high-performance Networking.

epoll was included in Linux 2.6 in 2003.
When epoll came, the internet was growing very fast, and developers were facing c10k problem.
C10k problem was the problem that one physical server could handle only up to 10 thousand concurrent connections; if the number is surpassed, the server would crash.

System before epoll

Before epoll() came in to existance, Linux use to have Select() and poll()

select():

This was the first event notification system developed by Unix, and it could handle only up to 1024 concurrent connections; it was used in the 1980s

Poll():

This system call came in the 1990s. It could handle any number of connections but only in theory; when 10k-plus connections come, the server crashes.

How epoll worked and how developers used it

epoll was included in Linux kernel version 2.6 in 2003.

Whenever data arrives on a socket, epoll sends a notification by pushing information into a doubly linked list which resides in RAM

struct
epitem {
struct
rbn_node rbn; // Red-Black Tree Node (Watchlist ke liye)
struct
list_head rdllink; // Doubly Linked List Node (Ready List ke liye)
int
fd; // Socket / File Descriptor
//

};
Software like a web server uses epoll in the following way: When no data is being received, software (like a web server) calls epoll_wait() and enters a sleeping/waiting state. When data arrives on a socket, the kernel updates epoll’s ready list and wakes up the waiting process to handle the incoming data. This process is different in poll(). When data arrives, poll() wakes up and checks every single connection one by one. Scanning thousands of idle connections burns 100% CPU.

Drawbacks of epoll

1. We already saw that epoll() has a c10k problem, where it solved it but struggled to maintain 10 thousand concurrent connections without any problem.

2. High Syscall & Context Switching Overhead

epoll relies heavily on system calls like epoll_ctl() and epoll_wait(). Every time an event occurs, the CPU must switch between User Space and Kernel Space. Under heavy traffic with millions of packets, these thousands of context switches cause high CPU overhead and latency spikes. We must remember that, at this point, nobody expected to overcome the context-switching issue, but this problem has been removed in io_uring, so we need to add Context Switching in drawback list.

3. Synchronous Read/Write (Notification, Not Async I/O)

epoll tells you when data is ready, but it doesn’t read or write the data for you. Think of epoll like an alarm clock:
A. It rings to tell you “Hey, data has arrived!”
B. But your server still has to stop what it’s doing and manually do the heavy work of copying (read/write) that data into memory.

4. No Support for Disk Files

epoll only works with networks, not disk files.
• Networks (Sockets) wait for data: Data comes over the internet at random times, so epoll has to wait and notify you when it arrives.
• Disk Files (PDFs, Images, Databases) don’t wait: The Linux kernel thinks disk files are always ready to be read immediately. Since there is nothing to “wait” for, epoll cannot be used for regular files on your hard drive.

4. Multithreading Complexity (Thundering Herd Problem)

When multiple thread workers share a single epoll instance:
• Thundering Herd: When a new connection arrives, the kernel might wake up all waiting threads at once—even though only one thread can handle the connection. The remaining threads waste CPU cycles waking up for nothing.
• Complex Tuning: Developers must use special flags like EPOLLONESHOT or EPOLLEXCLUSIVE to handle edge cases, adding significant architecture complexity.

What is io_uring and How Does It Work?

Advent of io_uring, the Meta factor

In 2018-19, modern hardware changed drastically

  • Ultra-fast NVMe SSDs appeared, capable of millions of I/O operations per second (IOPS).
  • 100GbE+ Networks became common in data centers.
  • Hardware was becoming faster, but developers noticed that in networking CPUs
    are wasting time making system calls (epoll_wait, read, write)

Enter Jens Axboe (Meta Engineer)

Meta has a team of talented Linux kernel developers.

Meta is also one of those companies that contribute to the Linux Kernel.
One of the kernel developers and block layer maintainers at Meta (Facebook) decided to fix this fundamental problem. And that developer was Jens Axboe,
He initially tried to improve Linux’s old Asynchronous I/O (aio), but found it too broken and limited (it didn’t support buffered reads, network sockets, or true async operations cleanly).
Fixing the main layer of Asynchronous IO was breaking existing things and becoming a major problem for the entire ecosystem of networking
Instead of patching old, broken interfaces, Jens built a completely new API from scratch. His goal was simple:

“What if User Space and Kernel Space could talk to each other without making system calls at all?”
He designed io_uring around two shared memory ring buffers (Submission Queue & Completion Queue). This design allowed the application to just “drop” tasks into a queue that the kernel reads directly—eliminating the constant context switching that plagued epoll.

Merge into Linux Kernel (2019)

Jens merged io_uring into Linux Kernel 5.1 in May 2019.
What started as a tool to speed up disk storage quickly evolved into a universal I/O…

添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论