cswork / libraryOpen practice workspace

Articles

Linux epoll Interview Questions: Level Triggering, Edge Triggering, and Partial Reads

Practice Linux epoll interview questions with level and edge triggering, partial reads, nonblocking I/O, EPOLLONESHOT, and a reproducible stall trace.

Your server reads half a message from a socket, returns to epoll_wait(), and appears to stall even though bytes remain. An interview on epoll is really asking whether you can distinguish readiness from a new readiness notification. The answer depends on the registration mode, whether the descriptor is nonblocking, and what the handler did before waiting again.

The behavior below follows the Linux epoll(7) manual; the incident trace and sample answers are original practice exercises, not candidate reports or a claim that a particular employer uses these questions. For adjacent operating-system practice, read the Linux file descriptor guide.

Question 1: What does epoll_wait() tell you?

It returns events for registered file descriptors whose requested conditions are ready. For a readable stream, that means a read operation can make progress under the relevant conditions; it does not mean an entire application message has arrived. TCP is a byte stream. A four-byte length prefix, a header, and a payload may arrive separately, or several messages may arrive in one read. The event loop therefore needs both an I/O rule and a framing rule.

An epoll instance holds an interest list of registrations and a ready list used by epoll_wait(). The application adds, modifies, or deletes registrations with epoll_ctl(). The returned mask may also signal a hangup or an error. A strong answer checks those conditions and then uses the actual read or write result to determine what progressed. A readiness notification is permission to attempt I/O, not a promise that the requested byte count will be transferred.

Under the default level-triggered behavior, an FD that remains readable can be reported again on a subsequent wait. Under edge-triggered behavior, the application must not depend on another event merely because unread data remains from the earlier readiness change. The man page's pipe example makes this distinction explicit: after two kilobytes arrive and a handler reads one kilobyte, another edge-triggered epoll_wait() might block despite the remaining kilobyte.

Question 2: Trace a partial read under each mode

Assume a nonblocking pipe has no data, then a writer makes 2,048 bytes available. The reader requests readiness and epoll_wait() reports it. The handler consumes only 1,024 bytes, leaving 1,024 buffered. The following table is an original trace of this specific setup, based on the man page's example; it does not claim all FDs or all future writes have identical timing.

StepLevel-triggered registrationEdge-triggered registration
2,048 bytes become readableRead readiness is reportedRead readiness is reported
Handler reads 1,024 bytes1,024 bytes remain readable1,024 bytes remain readable
Handler waits again without another readThe level is still ready, so readiness can be reported againA new edge is not guaranteed; the wait may block
Safe handler decisionContinue consuming or rely on the persistent level, while preserving fairnessContinue nonblocking reads until EAGAIN, then wait for a later readiness change

The trap is saying “edge triggered is faster because it sends one event.” Performance depends on workload and loop design; fewer repeated reports do not excuse leaving an FD partly serviced. Another trap is saying “one read emptied the socket.” A positive short read has context-dependent meaning. For a generic nonblocking edge-triggered handler, the reliable rule is to continue while progress is possible and stop when the operation reports EAGAIN or EWOULDBLOCK, a terminal condition, or an application-defined bounded handoff that preserves its ready state.

Do not treat EAGAIN as a server failure. For a nonblocking FD it says the operation would block now. That is the point at which an edge-triggered loop can safely return to waiting for a later readiness transition, provided the FD remains correctly registered. The Linux read(2) manual documents partial reads and nonblocking errors. A return of zero on a stream read indicates end of file; handle it as closure rather than spinning.

Question 3: Why make the descriptor nonblocking?

The epoll(7) manual recommends nonblocking descriptors with edge-triggered use. Once readiness has been reported, a handler can repeatedly attempt I/O until it reaches the EAGAIN boundary. Without nonblocking mode, a follow-up read after the available bytes are consumed could block the event-loop thread. One stalled client can then delay every other connection handled by that loop.

The procedure is not “read forever.” A busy connection can monopolize the loop if one handler drains an unbounded amount of arriving data. The manual discusses tracking ready descriptors in an application-side list so they can be processed fairly. A practical loop sets a byte or time budget, leaves the FD marked ready if that budget expires before EAGAIN, and resumes it later without assuming the kernel will deliver a second edge. That last detail is the difference between fairness and an accidental stall.

Writes have the same shape. A nonblocking write() can accept fewer bytes than the application queued or return EAGAIN. Keep the unwritten suffix in an output buffer and resume when writable. Registering every idle connection for write readiness can waste work because many sockets are writable most of the time; interest in EPOLLOUT is commonly managed according to queued output. The interview should connect the event mask with the output queue, not describe it as a fixed setting.

Question 4: What does EPOLLONESHOT change?

EPOLLONESHOT disables further event delivery for that registered FD after one event is retrieved. The application must rearm it with epoll_ctl(..., EPOLL_CTL_MOD, ...) when it is ready to receive another notification. This is a separate rule from level versus edge triggering. A level-triggered registration with one-shot behavior can also appear to stop producing events if the handler never rearms it.

One-shot registration can help coordinate multiple worker threads that share an epoll instance: a worker owns handling for an FD, then rearms after updating its state. That is an architecture choice, not a substitute for correct nonblocking reads, buffer management, and locking. If the handler exits on a parse error or early return before rearming, the FD may stay silent while the connection remains open. Log registration changes and handler exit paths in a debugging exercise.

An event already retrieved by one worker does not make later operations safe by itself. Another thread may close or replace the FD, and FD numbers can be reused. The application should maintain connection ownership and lifecycle state, including when it removes a registration. A polished interview answer separates kernel event semantics from application concurrency rules.

Question 5: How would you debug a stalled connection?

Start with a timeline: when bytes arrived, what epoll_wait() returned, which flags were registered, and what each read returned. Record FD, connection identity, event mask, byte count, errno for failures, buffer length after each operation, and whether EPOLL_CTL_MOD ran. Sampling only “epoll wait took 30 seconds” is insufficient. The wait can be behaving exactly as requested while the handler has left buffered data or forgotten to rearm.

Check the mode actually installed by the application: default level triggering, EPOLLET, and possibly EPOLLONESHOT. Then inspect whether the FD is nonblocking. Reproduce with a deterministic local socket or pipe and deliberately deliver a message in two pieces. Make the handler read only part of the first piece. If it returns to epoll_wait() in edge-triggered mode, the test should expose the stall risk. If the handler drains to EAGAIN but still stalls, examine framing state and one-shot rearming.

Separate three possible queues: kernel socket bytes, the application's received-byte buffer, and fully decoded messages waiting for business logic. Reading until EAGAIN only drains kernel-visible bytes; it does not automatically finish a length-prefixed frame or dispatch it. If a parser waits for a complete frame while the buffer already contains two complete frames, a later epoll event is the wrong trigger for processing the second. Process all complete frames already buffered, subject to a fairness budget.

Also look for a false negative in the test itself. epoll_wait() can be interrupted by a signal and return EINTR; timeouts and closing behavior are separate from the partial-read issue. Consult epoll_wait(2) and epoll_ctl(2) for syscall contracts. Do not equate a single stalled test with proof of an epoll kernel bug.

If the same connection stalls only under high load, inspect the handler's fairness budget. A task queue that marks a connection ready but never schedules it again can mimic a missing edge. Compare the count of FDs reported by epoll_wait() with the count actually serviced. That separates kernel notification from application dispatch and makes a targeted fix possible.

If multiple sockets share one event loop, capture one healthy connection alongside the stalled one. A single FD repeatedly consuming a large budget points toward starvation; a quiet ready list points toward registration or notification; an unscheduled ready FD points toward dispatcher bookkeeping. Those observations lead to different fixes even though all three produce the same user-visible timeout.

A complete interview answer

“I would first ask whether the FD is registered with default level triggering, EPOLLET, or EPOLLONESHOT, and confirm it is nonblocking. Under level triggering, unread data can make the FD appear again. Under edge triggering, a partial read may leave data without producing another edge, so I would keep reading until EAGAIN or a terminal result, preserve any partial application frame, and process complete frames in the buffer. If I impose a fairness budget before EAGAIN, I must keep that FD on an application ready list. With one-shot I would rearm explicitly. I would verify this with an event/read/rearm trace rather than guessing from a blocked epoll_wait().”

That answer earns its confidence from an observable sequence. It also states its assumptions: a nonblocking stream or pipe, a specified registration mask, and the application's own framing. In a real incident, the next action follows the trace. If no read was attempted, debug dispatch; if the read consumed half the available bytes, debug the drain rule; if the read reached EAGAIN, debug rearming and parser state.

Practice questionConnection to this scenario
Diagnose I/O, Memory, and CPU ConstraintsDistinguish an I/O wait from resource exhaustion.
Linux Host and Filesystem FundamentalsExplain FDs and kernel-visible I/O state.
Explain OS processes, threads, and memoryExplain how a blocked event-loop thread affects the process.
Troubleshoot a production server outageBuild a trace before choosing an incident fix.
Diagnose Storage I/O Performance and Identify the WorkloadContrast readiness bugs with storage I/O pressure.

Continue with the Linux file descriptor guide, then implement the 2,048-byte partial-read test in both modes. Your test should state exactly when another event is expected, when it merely might occur, and when the application must act without one.

Sources and further reading