After changing a small server to use an event loop, I began with select(). It is portable, familiar and perfectly adequate for a small number of descriptors. It also requires rebuilding descriptor sets and asking the kernel about the whole set on every pass, even when nearly everything is idle.

Linux has a more suitable interface for this shape of workload: epoll. The application creates an epoll instance, registers descriptors with epoll_ctl(), and waits for ready events with epoll_wait(). The interest list remains in the kernel, so the program changes it only when a connection is added, removed or needs different notifications.

The practical difference is that the returned array contains ready items rather than one bit for every descriptor I might care about. With thousands of quiet sockets and a handful of active ones, work follows activity more closely. This does not make the network faster. It removes repeated bookkeeping around sockets that had nothing to say.

I store a pointer or identifier in epoll_event.data so a returned event can find its connection state without another descriptor lookup. Registration then becomes part of the connection lifecycle. A socket enters the interest list after being made nonblocking and leaves before its state is destroyed. Getting that order wrong creates bugs that appear only when descriptors are reused, which is the computer’s way of encouraging humility.

Level-triggered operation resembles select(): readiness continues to be reported while data remains. Edge-triggered operation reports transitions and can reduce repeated notifications, but handlers must drain reads or writes until they return EAGAIN. Forget one partial operation and the connection may wait forever for an edge that already happened.

I started with edge triggering because it sounded faster. I changed back to level triggering while making the protocol correct, then measured. For this service, clarity won. Edge triggering can be appropriate, but it is not a ceremonial badge awarded to serious network programmers.

Errors and hangups require care too. A readiness event may carry several flags, and readable data can coexist with peer shutdown. The handler should consume valid bytes, handle partial writes and close the connection through one well-defined path. epoll reports conditions; it does not design the state machine.

For many Linux sockets, I strongly prefer epoll to stretching select() beyond its pleasant range. I keep the readiness layer small so another platform mechanism could replace it. The valuable architecture is explicit nonblocking state and ownership. epoll is an efficient Linux engine for that architecture, not the architecture itself.