Epoll and Kqueue: How Operating Systems Learned to Wait Efficiently
Recorded: Sept. 22, 2026, 6:10 a.m.
| Original | Summarized |
epoll and kqueue: How Operating Systems Learned to Wait Efficiently The Coding GopherSubscribeSign inepoll and kqueue: How Operating Systems Learned to Wait EfficientlyThe quiet machinery behind scalable I/OThe Coding GopherFeb 19, 20264117Share“The hardest part of I/O isn’t reading data. It’s knowing when to stop waiting.”Every high-performance server eventually runs into the same problem: waiting. Not waiting for CPU, not waiting for memory, but waiting for the outside world. Network sockets sit idle. File descriptors stall. Thousands of connections exist, but only a few are active at any given moment.Early operating systems handled this poorly. Modern systems do not—largely because of mechanisms like epoll on Linux and kqueue on BSD-based systems. These APIs fundamentally changed how software waits for I/O, making today’s highly concurrent servers possible.The problem: waiting doesn’t scaleAt a low level, I/O is blocking. You ask the kernel to read from a socket, and if no data is available, the kernel waits. If you have one connection, this is fine. If you have ten thousand, it’s disastrous.The naive solution is one thread per connection. Each thread blocks independently. This works until thread creation, context switching, and memory overhead overwhelm the system.The second attempt was polling.Why select and poll weren’t enoughEarly Unix systems introduced select and later poll as ways to wait on multiple file descriptors at once.Conceptually, they work like this: you give the kernel a list of file descriptors and ask, “Which of these are ready?” The kernel scans the list and tells you.That scan is the problem.Each call requires iterating over every file descriptor, even if only one is active. As the number of connections grows, the cost grows linearly. At scale, most of your CPU time is spent asking the kernel the same question over and over again.The real issue wasn’t that waiting was slow—it was that checking was expensive.The core insight behind epoll and kqueueepoll and kqueue are built around a simple but powerful idea:don’t repeatedly ask the kernel what you care about—tell it once, and let it notify you.Instead of passing a full list of file descriptors every time you want to wait, you register your interest once. After that, the kernel tracks readiness internally and wakes you up only when something changes.Waiting becomes proportional to activity, not capacity.epoll: Linux’s event notification systemOn Linux, this idea is implemented as epoll.epoll introduces a persistent kernel object called an epoll instance. You create it once, then register file descriptors along with the events you care about—readability, writability, errors, and so on.int epfd = epoll_create1(0); This site requires JavaScript to run correctly. Please turn on JavaScript or unblock scripts |
The challenge in high-performance systems involving input/output operations stems from the fact that waiting for external events does not scale efficiently when managing numerous idle connections. Traditional blocking I/O methods, where a thread waits for data on a socket, become disastrous when dealing with thousands of connections, leading to excessive context switching and memory overhead if implemented with a one-thread-per-connection model. Early attempts to manage this waiting, such as select and poll, addressed waiting on multiple file descriptors, but they suffered from inefficiency because they required the user space application to repeatedly scan the entire list of descriptors in the kernel to determine which are ready. This repeated checking resulted in a computational cost that grew linearly with the number of connections, making the process of checking itself the performance bottleneck rather than the actual waiting time. The core innovation behind epoll on Linux and kqueue on BSD-based systems was a paradigm shift: moving from a polling model where user space repeatedly pulls state from the kernel to a notification model where the kernel pushes events to the user space when an event occurs. This fundamental change in approach allows waiting time to be proportional to actual activity rather than system capacity, making high concurrency feasible. Instead of constantly asking the kernel about the status of many file descriptors, systems are designed to register interest once, allowing the kernel to internally track readiness and notify the application only upon a state change. epoll in Linux implements this notification system through a persistent kernel object called an epoll instance. A process creates this instance and then registers its file descriptors with it, specifying the events it is interested in, such as readability or writability. The kernel takes responsibility for tracking the readiness of these descriptors internally. When a process calls epoll_wait, it does not scan all registered descriptors; instead, the kernel returns only those whose state has actually changed. This drastically reduces the overhead associated with waiting, as the cost of the wait becomes proportional only to the number of active events, rather than the total number of connections. epoll supports different modes of operation, such as level-triggered, where notification persists as long as a condition holds, and edge-triggered, where notification occurs only upon a state transition. kqueue serves as the equivalent, more general event system on BSD systems, monitoring a broader range of events beyond simple I/O readiness. kqueue can monitor file changes, process lifecycle events, signals, timers, and sockets, treating all these as first-class events. This generality allows kqueue to manage diverse sources of asynchronous events through a single interface. The philosophical distinction between these mechanisms is whether the system is purely focused on I/O readiness, as seen in epoll, or if it is a system for handling all types of asynchronous events, as seen in kqueue. This mechanism allows the kernel to handle the complex bookkeeping internally, shifting the management burden away from the user application. This model enables modern concurrent runtimes, such as Go's scheduler, to efficiently manage goroutines by parking them when waiting for I/O signals, avoiding the blocking of OS threads, and achieving massive concurrency by exploiting the efficiency gained from kernel-level notification. Ultimately, epoll and kqueue represent an acknowledgment that in networked systems, idle time is a liability, and efficient waiting requires the kernel to proactively notify applications of activity rather than relying on expensive, repeated checks. |