· via Hacker News – Front Page (native)
Epoll and kqueue explained: the kernel APIs behind high-concurrency servers
A deep dive by The Coding Gopher, surfaced on Hacker News, explains how epoll and kqueue replaced per-call descriptor scans with one-time kernel registration, making modern concurrent servers viable.

A long-form essay by The Coding Gopher, "Epoll and Kqueue: How Operating Systems Learned to Wait Efficiently," reached the front page of Hacker News. It walks through the operating-system machinery that almost every high-performance server quietly depends on: the readiness-notification interfaces epoll on Linux and kqueue on BSD-derived systems, including macOS.
Waiting is the expensive part
The essay opens by reframing the problem: the hard part of network I/O is not moving data, it is knowing when there is something worth acting on. A busy server may hold thousands of open connections, yet only a handful are active at any moment.
At the kernel level, reads are blocking — request data from a socket that has none and the kernel parks the caller until some arrives. That is harmless with one connection and untenable with ten thousand. The obvious fix, one thread per connection, collapses under the combined weight of thread creation, context switching and memory overhead.
Early Unix answered with select, and later poll, which let a program hand the kernel a list of file descriptors and ask which ones are ready. The catch is that every call forces the kernel to walk the whole list even when a single descriptor is active, so cost scales with connection count rather than with actual work. As the essay frames it, waiting was never the slow part — checking was.
Register once, get notified
Epoll and kqueue are built on the same inversion: instead of repeatedly asking the kernel what you care about, you tell it once and let it notify you when something changes. The cost of waiting then follows activity rather than capacity.
Inside epoll
On Linux, epoll_create1 sets up a persistent kernel object — an epoll instance — identified by its own file descriptor. Descriptors are added, modified or removed with epoll_ctl, each registered together with the events of interest, such as readability, writability or error conditions. The kernel keeps the full set as an interest list; the subset with something to report forms the ready list. A call to epoll_wait then sleeps until events exist and returns only the affected descriptors, with no scan of everything registered. Closing a file descriptor also removes it from every epoll instance it belongs to.
Epoll offers two delivery modes. In level-triggered mode, events keep arriving as long as the condition holds — leave data unread in a socket and you will keep being reminded. In edge-triggered mode, notification fires only on the transition from not-ready to ready; miss it or fail to drain the buffer, and that descriptor may never be reported again. Edge triggering is more efficient but demands disciplined non-blocking code, and the author cautions it is fertile ground for subtle bugs.
Kqueue takes the idea further
BSD systems push the same concept wider. A kqueue is a kernel-managed event queue, and kevent change requests register lasting interests — not just socket readiness but also file changes, process lifecycle events, signals and timers, all through a single interface. Where epoll is deliberately minimal and specialised, kqueue is general and expressive, treating events themselves, rather than I/O readiness alone, as the primary abstraction.
From pull to push
The deeper shift is architectural. With select and poll, user space repeatedly pulls state out of the kernel; with epoll and kqueue, the kernel pushes events out as they happen. Idle connections become nearly free, and work is driven by activity. The essay points to Go's runtime as a demonstration: a call like conn.Read looks blocking, but the runtime puts the socket in non-blocking mode, registers it with epoll or kqueue, and parks the goroutine — no operating-system thread waits on the data. When the kernel signals readiness, the runtime wakes the goroutine and resumes exactly where it stopped, which is how Go delivers large-scale concurrency without exposing async plumbing to the programmer.
Why it matters
According to the essay, nearly every modern high-performance server rests on these mechanisms, directly or indirectly: they make it practical to hold tens of thousands of connections, multiplex I/O onto a small pool of threads, build event-driven runtimes without callback-heavy code, and separate logical concurrency from physical threads. For engineers working well above this layer, understanding it explains both why today's servers scale the way they do and where their classic failure modes — edge-triggered starvation among them — come from. The piece is a useful reference for anyone debugging network services or designing runtimes, and a reminder that the largest wins often come not from processing faster but from not paying for idle.
- #linux
- #bsd
- #kernel
- #networking
- #systems-programming