An eventfd object combines a kernel-maintained unsigned 64-bit counter with file-descriptor readiness. A write adds to the counter when the addition is permitted; a read consumes counter state. Because the same object participates in poll(), select(), and epoll(), a counter transition can also become an event-loop notification without a byte stream or message framing layer.
That compact interface has sharp semantics. Default reads drain the current value to zero, EFD_SEMAPHORE reads consume one unit, writes can block near the counter limit, and readiness indicates which operation can proceed rather than the number of logical events an application may have assigned to the counter.
The stored state is a counter, not a byte queue
eventfd() creates an object whose state is initialized from the supplied unsigned int value. Reads and writes use an eight-byte integer interface: a successful transfer is exactly sizeof(uint64_t). Buffers of another size do not turn the object into a partial-transfer stream.
A normal write interprets the eight bytes as an unsigned 64-bit integer and adds that value to the current counter. The value UINT64_MAX is not accepted as an input value. The counter itself is constrained so ordinary userspace writes cannot raise it beyond UINT64_MAX - 1.
uint64_t increment = 3;
ssize_t n = write(efd, &increment, sizeof(increment));This addition semantics differs from a pipe. Three writes do not create three independently addressable records. They contribute numeric state. If values 2, 3, and 5 are accepted before a default read, that read can return 10.
The distinction matters when the application maps counter units to work. eventfd preserves arithmetic accumulation, not producer boundaries.
Default reads exchange accumulated state for zero
Without EFD_SEMAPHORE, a successful read returns the current nonzero counter value and resets the counter to zero as one consumption operation.
uint64_t value;
ssize_t n = read(efd, &value, sizeof(value));If producers add work while a consumer is active, the value returned by a given read reflects counter state at that operation’s synchronization point. A later increment remains available for a later read rather than being silently folded into a value already consumed.
This makes default mode suitable for notification coalescing. Multiple producers can signal the same consumer, and the consumer can receive one accumulated value instead of one kernel object per notification. The application must still decide what the numeric value means. A returned value of 7 is only seven work units when every producer follows that contract.
With EFD_NONBLOCK, a read while the counter is zero fails with EAGAIN. Without nonblocking mode, the read waits for the counter to become nonzero.
Semaphore mode changes consumption, not production
Creating the object with EFD_SEMAPHORE changes successful reads. Each read returns 1 and decrements the counter by 1. Writes retain their additive behavior.
That mode makes the counter act like a counting gate at the read boundary:
counter = 3
read -> 1, counter = 2
read -> 1, counter = 1
read -> 1, counter = 0The kernel still does not preserve the identity of individual writes. A single write of 3 and three writes of 1 can produce the same sequence of semaphore reads. If producer identity, payload ordering, or per-message metadata matters, a queue or another IPC mechanism is required.
EFD_SEMAPHORE therefore changes the granularity of consumption without converting the object into a record channel.
Readability and writability expose counter constraints
An eventfd descriptor is readable when its counter is greater than zero. It is writable when at least the value 1 can be added without exceeding the maximum counter value allowed for ordinary writes.
These readiness states describe operation feasibility. Readability does not report the exact counter value. An event loop that receives a readable notification must perform a read to consume state and obtain the value defined by the selected mode.
Writability has a similar boundary. A writable indication means some valid addition can proceed; it does not guarantee that every proposed 64-bit increment fits. A large write can still block or fail with EAGAIN in nonblocking mode if adding that specific value would exceed the limit.
This is the same separation seen in many readiness APIs: readiness is a predicate over possible I/O, not a reservation for an arbitrary future operation.
Saturation creates backpressure at the write boundary
If an addition would make the counter exceed UINT64_MAX - 1, a blocking write waits until a read reduces the counter. With EFD_NONBLOCK, the same condition produces EAGAIN.
For normal userspace signaling, this gives the counter a finite capacity even though the limit is extremely large. Code must not treat writes as mathematically unbounded increments.
Linux also documents a special overflow state for the case where kernel subsystems signal an eventfd counter enough times to overflow the 64-bit counter. In that condition, poll() reports both readable and error status, and a read returns UINT64_MAX. Ordinary userspace write() calls cannot create that overflow because additions that would cross the userspace limit are blocked or rejected.
That distinction is implementation-level Linux behavior, not a generic property of counters exposed by other operating systems.
File-descriptor lifetime enables shared notification paths
An eventfd object follows Linux file-descriptor lifetime rules. Duplicated descriptors refer to the same underlying object, and descriptors inherited across fork() also refer to that object. The counter is therefore shared through those references rather than copied into independent counters.
EFD_CLOEXEC can prevent an unintended descriptor from surviving execve(). EFD_NONBLOCK sets nonblocking status on the underlying open file description used by the descriptor.
The descriptor can also be passed through a UNIX domain socket. That permits independently arranged processes to share the same counter object after descriptor transfer. The synchronization contract remains numeric: sharing the descriptor does not add message ownership or producer identity.
The kernel releases the object after all file descriptors referring to it have been closed. Application-level work represented by the counter has no separate persistence contract; closing the final descriptor discards access to that kernel object.
Event loops need a drain policy that matches the mode
With level-triggered epoll, a default-mode eventfd remains readable until a read drains its nonzero counter. In semaphore mode it remains readable while at least one unit remains, so a consumer may need repeated reads to consume all currently available units.
With edge-triggered notification, the consumer normally combines nonblocking I/O with a drain loop until EAGAIN. Stopping while consumable counter state remains can leave the descriptor readable without another readiness transition to prompt immediate work.
The required loop shape follows the selected consumption semantics. Default mode often drains in one successful read because that read resets the counter to zero. Semaphore mode can require many reads because each successful call removes exactly one unit.
This behavior also limits what an event notification proves. Receiving one epoll event does not correspond to one producer action, and receiving no new edge does not prove the counter is zero if the consumer previously stopped before draining it.
The contract is numeric synchronization
eventfd is strongest when a program needs a small kernel synchronization object that is both incrementable and visible to descriptor-based waiting. It can connect worker notification, completion accounting, or wakeup paths to an event loop without introducing stream parsing.
Its limits come from the same compactness. The kernel stores a number, not a history. Default mode coalesces accumulated units into one read; semaphore mode parcels those units out one at a time; readiness exposes operation state rather than message count. Designs that preserve those boundaries can use eventfd as a precise numeric synchronization contract instead of treating it as a miniature message queue.