Hello! Welcome to the next lesson in our journey through the backend.
In our last session, we contrasted different I/O models and discovered a fundamental challenge: how can a server handle thousands of connections without getting stuck (blocking) or wasting CPU cycles by constantly asking "is there data yet?" (polling). We identified I/O multiplexing as the solution, and mentioned that modern, high-performance systems use specific system calls like epoll on Linux to implement it efficiently.
Today, we will dive deep into that mechanism. Our goal is to describe the role of an event loop (e.g., using epoll/kqueue) in high-concurrency servers. You'll learn how a single thread can, almost magically, juggle thousands of network connections at once. This concept is the engine behind technologies like Node.js, Nginx, and asynchronous Python frameworks.
1. The C10k Problem: Why select() and poll() Weren't Enough
The first I/O multiplexing system calls, select() and poll(), were a huge step up from simple blocking I/O. They allowed a process to monitor multiple file descriptors (FDs) at once. However, they have a critical performance flaw that becomes apparent at scale, a challenge famously known as the C10k problem (handling ten thousand concurrent connections).
The issue is that with select() and poll(), your application has to pass the entire list of FDs you're interested in to the kernel every single time you want to wait for an event. The kernel then has to iterate over this entire list to check which ones are ready.
This means the work the kernel does is proportional to the number of connections you are watching (). If you're watching 10,000 connections, the kernel scans 10,000 FDs, even if only one of them has data. This is inefficient.
2. The Modern Solution: epoll, kqueue, and Event Notification
To solve this scalability problem, modern operating systems introduced more advanced APIs. Linux created epoll, and BSD systems (including macOS) created kqueue. While their APIs differ, they share the same core principle: an efficient event notification system.

The key innovation of epoll is that you don't pass the list of FDs to the kernel on every call. Instead:
- You create a persistent
epollinstance inside the kernel. - You use a control interface to register ("add") or de-register ("delete") FDs you are interested in with this instance. This is a one-time setup for each connection.
- You then simply ask the
epollinstance, "Are any of the FDs I registered ready?"
The kernel maintains the "interest list" internally. When a network packet arrives for a specific socket, the kernel already knows that this socket is part of an epoll set. It adds the socket to a "ready list" for that set. When your application calls epoll_wait(), the kernel only needs to check this ready list and return the FDs that have events.
The check is now proportional to the number of active connections (), not the total number of connections (). This is an operation, which is dramatically faster when you have many idle connections.
To learn more about these differences and the epoll API, let's start with a great article that breaks it down.
Mastering epoll: The Engine Behind High-Performance Linux Networking
The article 'Mastering epoll' provides a fantastic deep dive into how epoll works. It explains why it was created, how its API functions, and even what happens inside the kernel.
Please read the following sections: Section 1: 'Overview about epoll and why itâs different': This introduces epoll and contrasts it with select/poll. Section 7: 'Why epoll is different & Why this scales ?': This section reinforces the performance difference by explaining the cost of each approach.
3. A Look at the epoll API
Understanding epoll means understanding its three main system calls. Let's look at a C-level perspective, as this is the fundamental layer where these operations occur.
-
epoll_create1(flags): Creates a newepollinstance within the kernel and returns a file descriptor that represents it. This FD is your handle for all futureepolloperations. -
epoll_ctl(epoll_fd, op, target_fd, event): The control interface. This is how you manage the list of FDs monitored by theepollinstance.epoll_fd: The FD of yourepollinstance.op: The operation, typicallyEPOLL_CTL_ADD,EPOLL_CTL_MOD(modify), orEPOLL_CTL_DEL.target_fd: The FD you want to monitor (e.g., a socket).event: A struct that specifies what you're interested in (e.g.,EPOLLINfor readable data) and attaches some data (like thetarget_fditself) to be returned to you later.
-
epoll_wait(epoll_fd, events, maxevents, timeout): The workhorse. This call blocks (waits) until one of the monitored FDs has an event or the timeout expires.- It returns the number of ready FDs.
events: A buffer you provide, which the kernel fills with the event information for the ready FDs.
The following video gives a fantastic, code-driven walkthrough of this API.
All About Epoll - Scalable I/O Syscalls in Linux!
The video 'All About Epoll' by hoff._world provides a very practical demonstration of the epoll API using C code. This will help you see exactly how these system calls are used to build a concurrent application.
Please watch from the beginning to 15:40. Don't worry about compiling or running the C code yourself. Focus on understanding: The setup: How epoll_create1 is called to get an epoll instance. Registering FDs: How epoll_ctl with EPOLL_CTL_ADD is used in a loop to add several file descriptors (pipes, in this case) to the watchlist. Waiting for Events: How epoll_wait is the core of the main loop, blocking until the kernel reports that some FDs are ready. Handling Events: How the code iterates over the events returned by epoll_wait to handle only the FDs that need attention.
4. How the Event Loop is Constructed
Now we can put all the pieces together to define the event loop. An event loop is a design pattern that allows a single thread to handle many I/O operations without blocking. It is a simple while loop that continuously performs the same cycle:
- Poll for Events: Call
epoll_wait(), asking the kernel to put the process to sleep until an event occurs on any of the registered file descriptors. - Dispatch Events: When
epoll_wait()returns, it gives you a list of FDs that are ready for I/O. The loop iterates through this list. - Handle Events: For each ready FD, the loop performs the corresponding non-blocking operation:
- If the ready FD is the main listening socket, it means a new client is trying to connect. The loop calls
accept()to create a new client socket and registers this new socket withepollusingepoll_ctl_add. - If the ready FD is a client socket with a "read" event (
EPOLLIN), it means the client has sent data. The loop callsread()to get the data and then processes the request (e.g., parsing an HTTP request). - If the ready FD is a client socket with a "write" event (
EPOLLOUT), it means a previous write didn't complete and the socket's buffer is now ready for more data. The loop writes the response back to the client.
- If the ready FD is the main listening socket, it means a new client is trying to connect. The loop calls
This cycle repeats indefinitely. The thread is only ever doing work when there's actually work to be done. For the rest of the time, it's efficiently sleeping inside the epoll_wait() call, consuming zero CPU.

This pattern is not unique to Linux's epoll. The article below demonstrates how to build the exact same server architecture using kqueue on a BSD-like system (macOS).
Writing a Simple TCP Server Using Kqueue
The article 'Writing a Simple TCP Server Using Kqueue' shows the implementation of an event-driven server. Although it uses kqueue instead of epoll, the architectural pattern is identical and provides a clear example of the event loop in action.
Skim through these sections to see the pattern come to life: 'Design': Notice the diagram and the high-level steps, which mirror what we've discussed. 'Event Loop Type': See how kqueue() is called to create the instance and kevent() is used to register the initial listening socket (analogous to epoll_create1 and epoll_ctl). 'Event Loop Polling': Look at the for loop. This is the event loop. It calls kevent() to wait for events, then has an if/else if block to handle different event types: a closed connection (EV_EOF), a new connection, or incoming data (EVFILT_READ). 'Main Function': See how everything is tied together to start the server.
5. Kernel Internals and Real-World Use
To satisfy your goal of going to the "deepest level," let's briefly touch on what's happening inside the kernel. As mentioned in the resources, epoll uses highly efficient data structures:
- A Red-Black Tree to store all the file descriptors being monitored. This allows for efficient additions, modifications, and deletions (
epoll_ctl) in time. - A Linked List that serves as the "ready list." When a driver signals that an FD has an event, the kernel adds the item to this list.
epoll_wait()simply checks if this list is empty. If it's not, it returns the items from the list. This check is .
This combination of data structures is what gives epoll its incredible performance.
The Node.js runtime is a prime example of an event loop architecture. The underlying C++ library, libuv, provides a cross-platform event loop that uses epoll on Linux.
Why does node.js scale? Libuv & epoll & fcntl
The video 'Why does node.js scale?' connects these low-level OS primitives directly to the Node.js event loop, explaining how libuv uses epoll to achieve its famous concurrency.
Watch these clips to see the connection: fcntl and Non-Blocking Sockets (06:11 - 07:58): A quick review of why sockets must be set to non-blocking mode for this to work. select, poll, and epoll (07:58 - 12:11): A great summary of the performance differences and the internal data structures (like the Red-Black Tree) that make epoll fast. How Libuv uses epoll (14:54 - 16:17): This brings it all together, explaining that at the very core of Node's event loop is a call to epoll_wait (or its equivalent on other OSes).
Conclusion
You now have a deep understanding of one of the most important concepts in modern backend engineering. The event loop isn't magic; it's a clever programming pattern built on top of powerful and efficient OS primitives like epoll and kqueue.
Key Takeaways:
- Older I/O multiplexing (
select,poll) scales poorly because it requires the kernel to scan all monitored connections on every call (). epollsolves this by having the kernel maintain an "interest list." The application registers FDs once, and the kernel efficiently notifies the app when events occur. A check is now proportional to the number of active connections ().- An event loop is a single-threaded loop that calls
epoll_wait()to sleep until I/O is ready, then wakes up to handle only the active connections before going back to sleep. - This architecture allows a single thread to concurrently manage thousands of connections, forming the foundation of high-performance servers like Nginx and runtimes like Node.js.
Preview of the Next Lesson:
We've covered a lot of low-level theory. In the next module, we'll start putting this into practice. Our first step will be to build a minimal TCP server in Python using the socket module. This will give you a hands-on feel for the system calls (bind, listen, accept) that form the basis of any network server, before we move on to see how frameworks and web servers automate this for us.