edge-triggered versus level-triggered notification
Think of a doorbell versus a 'door open' light. A level-triggered light stays ON the whole time the door is open - glance up any time and you can see it. An edge-triggered doorbell rings ONCE at the moment the door opens, and never again, even if it stays open. A readiness notifier can work either way, and getting the difference wrong is one of the classic ways to hang a high-performance server.
Level-triggered (the default for epoll, and how select/poll behave): as long as a socket HAS unread data, every call to the waiter reports it ready, again and again, until you have drained it. This is forgiving - if you read only some of the bytes this time, you will simply be told 'still ready' next time. Edge-triggered (epoll with the EPOLLET flag): you are told 'ready' only on the TRANSITION from not-ready to ready - the instant new data arrives. If you do not read everything available right then, you will NOT be told again until even more data arrives. The discipline this forces: when an edge-triggered descriptor fires, you must read in a loop until read() returns EAGAIN, proving you have drained the socket completely.
Why it matters and the hazard: edge-triggered fires fewer events, which can mean fewer wakeups under heavy load, and it pairs naturally with non-blocking descriptors. But the failure mode is brutal - if you forget the drain-until-EAGAIN loop, leftover bytes sit unnoticed and the connection appears to silently stall forever, since the edge already fired and will not fire again. Level-triggered is the safer default for most code; reach for edge-triggered only when you have measured a real benefit and will respect its draining contract.
ev.events = EPOLLIN | EPOLLET; /* edge-triggered */ /* on fire, you MUST drain: */ for (;;) { ssize_t n = read(fd, buf, sizeof buf); if (n <= 0) { if (errno == EAGAIN) break; /* else closed/error */ } }
With EPOLLET you are notified only on the edge, so you must loop reading until EAGAIN or risk leaving data unread forever.
Edge-triggered descriptors basically demand non-blocking mode: the drain loop relies on read() returning EAGAIN to know when to stop, and a blocking read() would freeze the thread on the final empty read instead. Level-triggered with EPOLLONESHOT is a common middle ground in multithreaded servers to avoid two threads handling the same descriptor.