Free — no signup required

Generation 2: Epoll (The Event List)

2 min read

The "Don't Call Us, We'll Call You" Era

Linux 2.6 introduced epoll, which inverted the model. Instead of the application polling the kernel, the kernel notifies the application.

The epoll API has three calls:

// 1. Create an epoll instance (returns a file descriptor)
int epfd = epoll_create1(0);

// 2. Register interest in events on a file descriptor
struct epoll_event ev;
ev.events = EPOLLIN;  // Notify when data is available to read
ev.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);

// 3. Wait for events — returns ONLY the ready file descriptors
struct epoll_event events[MAX_EVENTS];
int nready = epoll_wait(epfd, events, MAX_EVENTS, timeout_ms);

// Only iterate over the nready events — not all 10,000 connections
for (int i = 0; i < nready; i++) {
    handle(events[i].data.fd);
}

The kernel maintains an internal ready list. When a network packet arrives for a connection, the kernel adds that connection's fd to the ready list. epoll_wait() returns only the fds on the ready list — not all monitored fds.

Complexity: O(1) per event, O(N) to register N file descriptors (done once).

Edge-triggered vs. Level-triggered:
- Level-triggered (default): epoll_wait returns as long as data is available. Safe but can cause repeated wakeups.
- Edge-triggered (EPOLLET): epoll_wait returns only when the state changes (new data arrives). More efficient but requires the application to drain the entire buffer on each notification, or it will miss events.

epoll is the foundation of Nginx, Node.js's event loop, Go's netpoller, and virtually every high-performance network server written in the last 20 years.

Interview Tip

A common interview question is: "How does Node.js handle 10,000 concurrent connections with a single thread?" The answer is epoll (on Linux) via libuv. Node.js registers all socket fds with epoll. The event loop calls epoll_wait(), which returns only the sockets with data. Node.js processes those callbacks, then calls epoll_wait() again. No thread-per-connection is needed because the thread is never blocked waiting — it is always doing useful work or sleeping efficiently in epoll_wait().

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free