Linux Internals / Under the Hood

epoll Explained: Readiness, Edge Triggers, and Backpressure

Understand Linux epoll readiness semantics, level- versus edge-triggered handling, EAGAIN, writable interest, and connection lifetime pitfalls in event-driven servers.

Palash Vij / Published / 3 min read
Source-backed explainer Linux epoll Sockets Concurrency

An event loop receives a readable notification, reads part of a request, then waits forever for another event even though data remains buffered. That is a classic sign that the program treated an edge-triggered notification like a recurring reminder.

epoll reports readiness for I/O; it does not perform the I/O, parse a complete message, or guarantee a future operation will not encounter a race. Keep those responsibilities separate.

Interest and readiness are different sets

An epoll instance tracks the file descriptors and event types registered through epoll_ctl. The kernel makes relevant ready entries available to epoll_wait. This avoids repeatedly scanning every watched descriptor in the same way a simple polling loop would.

It does not make all event-loop work constant time. Dispatch, copying, parsing, connection management, and the amount of actual I/O still matter. A server with many mostly idle connections is different from one with every connection continuously active.

Level-triggered is the simpler starting point

In level-triggered mode, readiness can be reported while the condition remains true. With edge-triggered behavior, the application must not rely on another event merely because it left data unread.

Use nonblocking descriptors and handle the operation’s result explicitly:

repeat receive:
  positive bytes -> append and parse within resource limits
  zero bytes     -> peer performed orderly shutdown
  EINTR          -> retry as appropriate
  EAGAIN         -> no more data available now; return to scheduling
  other error    -> record and close or recover deliberately

This is algorithmic pseudocode, not a complete socket implementation. The stream may contain a partial message or several messages. recv boundaries are not application protocol boundaries.

Draining and fairness must coexist

“Read until EAGAIN” is a correctness rule of thumb for edge-triggered handling, not permission to let one busy connection monopolize a thread indefinitely. A production loop can keep an application-level ready queue and resume work after a bounded service slice without forgetting that the descriptor may still be ready.

Backpressure also matters. If parsing or downstream work cannot keep up, unlimited buffering simply moves the problem into process memory. Bound input, decoded requests, output, and downstream queues with explicit policies.

Writable interest can cause a busy loop

A socket is often writable when some send-buffer capacity is available. Watching EPOLLOUT permanently can wake a loop even when it has nothing to send. Enable writable interest when pending output cannot be completed immediately, and disable it when the output queue drains.

A partial send is normal. Preserve unsent bytes and their ownership; do not free a buffer because one send call returned a positive value.

EPOLLONESHOT can help coordinate handing a connection to one worker, but it requires explicit rearming. Forgetting to rearm can look like an idle connection even while work remains.

Test lifecycle races, not just the happy path

In an authorized local test, vary message fragmentation, slow readers, disconnect timing, simultaneous readiness, and queued output. Record queue limits and how the loop behaves when they are reached. A tiny echo test does not validate a server’s backpressure design.

File descriptor numbers are reused. Events and application objects need lifetime rules so an old event is not applied to a new connection that inherited the same number. Closing a descriptor and deleting every related reference are not necessarily one atomic application operation.

Readiness is not completion

io_uring exposes different submission/completion mechanisms, but changing APIs does not remove ownership, partial work, cancellation, or overload concerns. Choose an I/O model based on a measured workload and a clear lifecycle design.

Benchmark with the intended traffic distribution and record kernel, CPU, connection count, message sizes, queue bounds, and latency. The most valuable first improvement is often a correct event state machine, not a different notification API.

Further reading

Primary references behind this explanation. Consult documentation for the version and environment you actually run.

  1. Linux epoll(7)
  2. Linux epoll_ctl(2)
  3. Linux recv(2)
Follow the thread

Related reading

Search the notebook

Search articles, projects, and experiments. Everything stays on your device.

palash / notebook

A small doorway into the site. No shell, no remote execution.

Welcome. Type help to find your way around.

lab →experience →contact →Ctrl / Cmd + K