
Linux Poll() vs Epoll: When to Use Each Syscall
Reach for poll() when you're watching a modest, changing set of file descriptors and want behavior that works on any POSIX system. Use ppoll() when you also need to unblock signals only during the wait without a race. Move to epoll once you're past a few hundred descriptors, because poll's full rescan on every call is what actually breaks at scale. And before you file any "linux poll is broken" bug, run strace on the process first. Half of those reports are EINTR handling or a misread revents mask, not the syscall.
Last updated: 2026-07-21
Polling in Linux lets one process watch many file descriptors and learn which are ready for I/O, like reading or writing. Network servers, user interfaces, and event-driven programs all juggle many I/O sources at once. Polling handles all of them in one process, without a thread per source and without blocking on any single one. That is the backbone of I/O multiplexing on Linux, and it is what lets a server scale past a handful of connections.
What does poll do in Linux?
poll() is the system call that blocks on a set of file descriptors until at least one becomes ready for I/O or a timeout fires. It sits in the same family as read, write, select, and epoll. Instead of asking "is this one file ready" and stalling, you hand the kernel a list and ask "tell me which of these is ready."
Here is what happens: your process passes an array of struct pollfd and a timeout. The kernel checks each descriptor, and if none is ready, it puts your process to sleep. When any watched descriptor becomes readable, writable, or errors out, the kernel wakes you and marks which ones fired. You then act only on the ready ones.
This is synchronous I/O. Your process actively waits for the readiness event, unlike true asynchronous I/O where the kernel fires a signal or callback when work completes. For most server code, synchronous multiplexing with poll is exactly what you want, because the control flow stays readable.
How poll works under the hood
poll() walks your entire array on every single call. There is no fd_set bitmask like select uses and no kernel-side memory of what you asked last time. You give the full list, the kernel scans all of it, sets the revents fields, and returns. Next call, it does the whole walk again.
That walk is the scaling limit. With ten descriptors it is nothing. With ten thousand, the kernel copies and scans the whole array every wakeup, and your CPU burns cycles checking sockets that had no traffic. The cost grows linearly with the number of descriptors you watch, the textbook O(n) problem.
Run strace -e poll ./yourprogram and you see it plainly. Each poll(...) line shows the full array going in and the ready count coming back. That output tells you whether you are actually blocking as intended or spinning in a tight loop because a timeout or a stale event mask is wrong. When someone insists poll is misbehaving, this is the first thing I look at, because the syscall almost never lies.
A minimal poll example in C
Here is a compilable linux poll example. It watches standard input for readable data and standard output for write readiness, then blocks until one fires:
#include <poll.h>
#include <unistd.h>
#include <stdio.h>
int main(void) {
struct pollfd fds[2];
int ret;
fds[0].fd = 0; // stdin
fds[0].events = POLLIN; // ready to read
fds[1].fd = 1; // stdout
fds[1].events = POLLOUT; // ready to write
ret = poll(fds, 2, -1); // block until something is ready
if (ret > 0) {
if (fds[0].revents & POLLIN)
printf("Data is available to read on stdin.\n");
if (fds[1].revents & POLLOUT)
printf("File descriptor 1 is ready for writing.\n");
} else if (ret == 0) {
printf("poll timed out.\n");
} else {
perror("poll");
}
return 0;
}
Compile it, run it under strace, and pipe something into stdin to watch the POLLIN fire. Notice the & when I check revents. That is a bitwise test, not an assignment, and getting it wrong is a classic way to break your event handling silently. If you want the broader picture of how these calls fit together, the step-by-step Linux system calls guide covers the surrounding machinery.
The pollfd structure and its event flags
Each descriptor you watch is one struct pollfd with three fields: fd is the descriptor, events is what you want to know about, and revents is what actually happened. You fill in fd and events. The kernel fills in revents. Mix those two up and you will chase a phantom bug for an hour.
The flags you set in events:
- POLLIN – data is available to read.
- POLLOUT – the descriptor can be written without blocking.
- POLLPRI – urgent out-of-band data is present.
The flags the kernel can set in revents even if you never asked for them:
- POLLERR – an error condition on the descriptor.
- POLLHUP – the peer hung up; the other end closed.
- POLLNVAL – the descriptor is not open. You handed poll a bad
fd.
The mistake I see most is checking events after the call instead of revents. Your events field never changes; it is your request. Only revents holds the result. And always test POLLERR, POLLHUP, and POLLNVAL, because the kernel reports them whether or not you listed them, and ignoring them means you keep polling a dead socket forever.
Timeout and blocking behavior explained
The third argument to poll() is a timeout in milliseconds, and it has three cases. Pass -1 and poll waits indefinitely until a descriptor is ready. Pass 0 and it returns immediately after one check, ready or not, which is a non-blocking poll. Pass a positive number and it blocks up to that many milliseconds, then returns even if nothing fired.
Misreading this argument is where the busy-loop bugs come from. If you meant to block forever but passed 0, your loop spins at full CPU calling poll over and over, doing nothing useful. strace shows this instantly: hundreds of poll(...) = 0 lines a second means you told it not to wait.
The opposite bug is passing -1 when you actually needed a periodic wakeup, and then your program hangs because no event ever arrives and you never time out. Pick the timeout that matches the loop you are writing, and confirm it with a real trace rather than assuming.
Reading poll's return values and errors
The return value has three cases, and each means something different. A value greater than zero is the count of descriptors with events waiting, so you scan the array for non-zero revents. A return of 0 means the timeout expired with nothing ready. A return of -1 means the call failed, and errno tells you why.
The one everybody trips over is EINTR. If a signal is delivered while poll is blocked, poll returns -1 with errno set to EINTR. This is not an error in your logic. It means a signal interrupted the wait, and the correct response is usually to loop and call poll again. Treat EINTR as a real failure and you will get spurious crashes that only show up under load, when signals are flying.
You may also see EFAULT if the array pointer is outside your address space, or EINVAL if the descriptor count is nonsense. When the errno path confuses you, read the poll manual page rather than guessing from a forum snippet. The poll(2) man page lists every error case exactly as the kernel returns it. Understand the failure or it comes back.
What ppoll adds over poll
ppoll() is the Linux variant that adds two things: a timespec timeout measured in nanoseconds instead of milliseconds, and a signal mask applied atomically during the wait. The signature takes the same pollfd array, then a struct timespec *, then a const sigset_t *. That last argument is the whole reason ppoll exists.
Plain poll has a race. If you want to block certain signals except while poll is waiting, you unblock them and then call poll as two separate steps. A signal can arrive in the gap between the two, get handled, and then poll blocks forever waiting for an event the signal was supposed to announce. That is a real hang, and it is miserable to reproduce.
ppoll() closes the gap by swapping in your signal mask and calling poll as one uninterruptible operation, then restoring the old mask on return. The signal can only fire while poll is actually blocked, so your wakeup logic works. If you are mixing polling with signal-driven shutdown, read the signal handling walkthrough alongside this, because the two interact more than people expect.
When should you use ppoll instead of poll?
Reach for ppoll() when your code needs to unblock signals only during the wait and nowhere else. The classic case is a server that keeps SIGTERM blocked during request processing, so cleanup never runs half-finished, but wants to be woken by SIGTERM while it sits idle in the poll loop. That atomic mask swap is the only correct way to do it.
If you have no signals in play, ppoll buys you nothing except a slightly finer timeout resolution, and plain poll is simpler and more portable. Check the exact signature with man ppoll before you wire it in, because the argument order trips people who copy the poll call and just add a mask on the end.
Is poll part of POSIX or is it Linux-specific?
poll() is POSIX-standard and portable across essentially every Unix-like system, including the BSDs and macOS. If portability matters, poll is a safe choice. ppoll() is not POSIX. It is a Linux extension, with a similar pselect() cousin in the standard and a variant on some BSDs, so code that calls ppoll will not compile cleanly everywhere.
You can confirm the portable surface in The Open Group POSIX specification, which defines poll's contract. Anything beyond that, like ppoll's signal mask, you should treat as platform-specific and guard accordingly.
How does poll compare to select and epoll?
Use select almost never, poll for modest descriptor counts, and epoll once you scale. That is the short version. Here is the tradeoff laid out:
| Mechanism | How it tracks fds | Scaling cost | fd limit | Portability |
|---|---|---|---|---|
select | Three fd_set bitmasks | O(n) rescan every call | Capped by FD_SETSIZE, usually 1024 | POSIX, oldest, most portable |
poll | Array of struct pollfd | O(n) rescan every call | No hard cap | POSIX, widely portable |
epoll | Kernel-managed interest list | O(1) ready list | System-limited, very high | Linux only |
select is the oldest and the weakest. Its hard descriptor ceiling and clumsy bitmask API make it a poor default; the only reason to pick it is portability to something ancient. poll fixes the ceiling and gives you a cleaner per-descriptor structure, but it still rescans the whole array on every call.
My rule: below a few hundred descriptors, poll is fine and its simplicity is worth more than epoll's speed. Above that, the O(n) rescan starts eating measurable CPU on idle connections, and epoll's design wins. The file descriptor management guide is worth a read if you are juggling enough of them to care about this line.
Why epoll beats poll at scale
epoll avoids the full rescan by keeping state in the kernel between calls. It was introduced in Linux kernel version 2.5.45, specifically to handle large numbers of descriptors that poll and select choke on. Instead of passing the whole list every time, you register descriptors once and the kernel maintains a ready list for you.
Three calls do the work. epoll_create1() returns a descriptor for a new epoll instance. epoll_ctl() adds, modifies, or removes a descriptor from the instance's interest list with EPOLL_CTL_ADD, EPOLL_CTL_MOD, and EPOLL_CTL_DEL. epoll_wait() blocks until something is ready and returns only the descriptors that fired.
#include <sys/epoll.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) { perror("epoll_create1"); exit(EXIT_FAILURE); }
struct epoll_event event = { .events = EPOLLIN, .data.fd = 0 };
struct epoll_event events[10];
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, 0, &event) == -1) {
perror("epoll_ctl"); exit(EXIT_FAILURE);
}
int n = epoll_wait(epoll_fd, events, 10, -1);
for (int i = 0; i < n; i++)
if (events[i].data.fd == 0)
printf("Data is available to read on stdin.\n");
close(epoll_fd);
return 0;
}
The speed comes from that split. epoll_wait hands you only the ready descriptors, so a server with 10,000 idle connections and 3 active ones does work proportional to 3, not 10,000. Poll would scan all 10,000 every time. That is the entire reason epoll exists, and it is why every high-connection Linux server is built on it.
FAQ
Does poll modify my events field or reset revents between calls?
Poll never touches events; that stays exactly as you set it. It does overwrite revents on every call, so you do not need to clear it yourself. What you do need to reset is any application state you keyed off the last result, because a fresh call gives you a fresh set of ready flags with no memory of the previous one.
Can poll watch regular files, or only sockets and pipes?
You can add a regular file descriptor to a pollfd array, but it will almost always report ready for both read and write immediately, because disk files are considered always ready. Poll is built for descriptors that block, like sockets, pipes, and terminals. For real asynchronous disk I/O you want a different interface entirely, not poll.
Why does my poll loop hit 100% CPU?
You are almost certainly passing a timeout of 0 when you meant -1, so poll returns instantly and your loop spins. Trace it with strace -c and check the poll call count. A second common cause is a descriptor stuck reporting POLLHUP that you never remove from the array, so poll returns ready every call and you never actually consume or close it.
Is ppoll's nanosecond timeout actually more precise than poll in practice?
The timespec gives you nanosecond granularity in the API, but the real resolution is bounded by the kernel timer tick and scheduler, so do not expect nanosecond-accurate wakeups. The precision that matters is the atomic signal mask, not the timeout units. If you only care about the finer timeout and have no signals, the practical gain over poll is small.
Should I skip straight to epoll and never learn poll?
No. Poll is simpler, portable, and the right tool below a few hundred descriptors, and understanding its rescan model is exactly what makes epoll's design click. Learn poll first, watch it in strace, feel where it slows down, then move to epoll when you have the descriptor count to justify the extra complexity.
