Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
Why This Matters
If you are using epoll, this is the concept you absolutely must understand. Getting it wrong causes subtle bugs where data silently sits unread in a buffer forever — and your server appears to hang for certain clients.
The terms come from electronics: level-triggered and edge-triggered describe when a circuit responds to a signal — based on the signal’s current level, or based on the transition (edge) in the signal.
The Core Difference — Simple Analogy
Visualizing the Difference — Timeline
Scenario: 100 bytes arrive. You read 50 bytes. 50 bytes remain unread.
| Event | Level-Triggered (LT) | Edge-Triggered (ET) |
|---|---|---|
| 100 bytes arrive on socket | ✓ Notified | ✓ Notified |
| You read 50 bytes (50 left) | — | — |
| Next epoll_wait() call | ✓ Notified again (50 bytes still there) | ✗ NOT notified (no new data arrived) |
| New 10 bytes arrive | ✓ Notified (60 bytes now) | ✓ Notified (triggered by new arrival) |
Level-Triggered epoll — The Safe Default
/* Level-triggered is the DEFAULT in epoll — just don't add EPOLLET */
struct epoll_event ev;
ev.events = EPOLLIN; /* No EPOLLET — this is level-triggered */
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
/* In event loop — with LT you can read partial data safely */
if (events[i].events & EPOLLIN) {
char buf[512];
ssize_t n = read(fd, buf, sizeof(buf)); /* Read what we can */
if (n > 0) {
process(buf, n);
/* If 2000 bytes were available but we only read 512,
next epoll_wait() will notify us again — safe */
}
}
Edge-Triggered epoll — The Fast But Tricky Mode
/* Edge-triggered: add EPOLLET flag */
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; /* Edge-triggered */
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
/* RULE: With ET, you MUST read until EAGAIN every single time */
if (events[i].events & EPOLLIN) {
char buf[512];
ssize_t n;
/* Keep reading until no more data (EAGAIN/EWOULDBLOCK) */
while (1) {
n = read(fd, buf, sizeof(buf));
if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
/* All data consumed — stop reading */
break;
}
/* Real error */
perror("read");
close(fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
break;
}
if (n == 0) {
/* EOF — client disconnected */
close(fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
break;
}
/* Process data */
process(buf, n);
/* Loop continues to drain ALL available data */
}
}
Setting Nonblocking — Required for Edge-Triggered
#include <fcntl.h>
void set_nonblocking(int fd)
{
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {
perror("fcntl F_GETFL");
return;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {
perror("fcntl F_SETFL");
}
}
/* Usage in accept() loop: */
int conn_fd = accept(listen_fd, NULL, NULL);
set_nonblocking(conn_fd); /* Must do this BEFORE */
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev); /* Adding to epoll */
The Starvation Problem with Edge-Triggered
Imagine fd A has 1MB of data and fd B has 100 bytes. With edge-triggered and a loop “read until EAGAIN”, you will spend a long time draining fd A’s 1MB before you even look at fd B. fd B’s client is waiting.
This is the starvation problem. One solution is to maintain a list of fds that have been notified but not fully drained, and round-robin through them.
When to Use Level-Triggered vs Edge-Triggered
| Situation | Use | Reason |
|---|---|---|
| Simple server, easier to reason about | Level-Triggered | Forgiving — partial reads are safe |
| Maximum performance, experienced developer | Edge-Triggered | Fewer kernel-user transitions |
| Multi-threaded server | ET + EPOLLONESHOT | Prevents concurrent event delivery per fd |
| Porting from select()/poll() | Level-Triggered | Behavior matches select()/poll() semantics |
Which I/O Model Uses Which Trigger Model?
Interview Questions
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
