A timerfd becomes readable after its timer expires. The notification is not a signal handler invocation and not a byte-stream message. Linux records pending expirations on a timer object and exposes that state through a file descriptor, so a timer can occupy the same readiness boundary as sockets, pipes, and other descriptors.

That interface does more than replace one notification mechanism with another. Clock selection determines the time domain, arming flags determine whether a deadline is relative or absolute, and each successful read reports the number of expirations accumulated since the preceding successful read or timer reconfiguration.

Clock selection defines the timer’s time domain

timerfd_create() creates a timer object and returns a descriptor referring to it. The clockid argument selects the clock against which the timer advances. CLOCK_MONOTONIC is nonsettable and excludes time spent while the system is suspended. CLOCK_BOOTTIME is also monotonic but includes suspend time.

CLOCK_REALTIME follows the settable system-wide wall clock. An absolute timer on that clock therefore has different semantics from a monotonic deadline. A discontinuous wall-clock adjustment can move the clock across an absolute deadline, while a monotonic clock is not subject to wall-clock setting changes.

The alarm variants, CLOCK_REALTIME_ALARM and CLOCK_BOOTTIME_ALARM, can wake a suspended system when the caller has CAP_WAKE_ALARM. That property belongs to those clock choices; creating an ordinary CLOCK_MONOTONIC timerfd does not imply wake-from-suspend behavior.

Arming separates relative delay from absolute deadline

timerfd_settime() uses an itimerspec containing an initial expiration in it_value and an optional repetition interval in it_interval. A zero it_value disarms the timer. A nonzero it_value arms it, and a zero interval makes the timer one-shot.

Without TFD_TIMER_ABSTIME, it_value is interpreted as a duration relative to the selected clock’s current value at the call. With TFD_TIMER_ABSTIME, it is interpreted as an absolute clock value.

struct itimerspec spec = {
    .it_value = { .tv_sec = 5, .tv_nsec = 0 },
    .it_interval = { .tv_sec = 1, .tv_nsec = 0 },
};

int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
timerfd_settime(fd, 0, &spec, NULL);

This timer first expires five seconds after arming, then repeats at one-second intervals. The kernel may schedule execution after an expiration instant; the API represents expiration state, not a promise that user-space code runs at the exact deadline.

Absolute deadlines are useful when repeated rearming must target a particular clock coordinate rather than accumulate relative delay. Their behavior still depends on the selected clock, so an absolute CLOCK_REALTIME deadline and an absolute CLOCK_MONOTONIC deadline are not interchangeable time contracts.

Read returns an accumulated expiration count

A successful read() supplies an unsigned eight-byte integer in host byte order. Its value is the number of expirations that occurred since the timer was last configured or since the previous successful read.

uint64_t expirations;
ssize_t n = read(fd, &expirations, sizeof(expirations));

For a periodic timer, delayed consumption does not require one queued record per tick. If five intervals expire before the consumer reads, the returned counter can be 5. The count preserves missed periodic ticks as an aggregate, but it does not preserve a separate timestamp for each expiration.

When no expiration is pending, a blocking descriptor waits. With TFD_NONBLOCK, the same read fails with EAGAIN. A buffer smaller than eight bytes produces EINVAL.

This aggregation matters for event-loop code. One readiness notification can correspond to multiple timer intervals, so treating each readable event as exactly one elapsed period loses information already supplied by the kernel.

Readiness reflects pending expiration state

select(), poll(), and epoll() report the timerfd as readable when one or more expirations are pending. A successful read consumes the accumulated expiration count. If no later expiration has occurred, the descriptor ceases to be readable.

The descriptor therefore represents level state:

pending expirations = 0  -> not readable
pending expirations > 0  -> readable
successful read           -> consume pending count

This boundary composes naturally with an event loop because timer state and I/O readiness use the same multiplexing interface. It does not make timer execution synchronous with I/O processing. Scheduler latency, other ready descriptors, and application work can all delay the read after the kernel has marked the timerfd readable.

Descriptor copies inherited through fork() refer to the same timer object. Reads through those descriptors consume expiration state from that shared object rather than independent timer copies. When all descriptors referring to the object are closed, the timer is disarmed and kernel resources are released.

Wall-clock cancellation exposes discontinuous clock changes

For an absolute timer based on CLOCK_REALTIME or CLOCK_REALTIME_ALARM, TFD_TIMER_CANCEL_ON_SET requests cancellation if the real-time clock undergoes a discontinuous change. After such a change, a current or later read can fail with ECANCELED.

That error is an observable clock-state transition, not an ordinary timer expiration. Code using cancel-on-set must therefore keep ECANCELED distinct from EAGAIN and from a successful expiration-count read.

The flag only applies with TFD_TIMER_ABSTIME on the supported real-time clocks. It does not turn monotonic timers into clock-change detectors, and it does not apply to a relative real-time timer.

Expiration counts define the interface boundary

timerfd combines three separate contracts: a selected Linux clock, a timer configuration, and descriptor readiness backed by an accumulated expiration count. The file descriptor does not carry per-expiration payloads, and readiness does not guarantee prompt user-space execution.

Its useful boundary is precise. Time progression remains a clock property, expiration remains a timer property, and consumption becomes ordinary descriptor I/O. Event loops can therefore incorporate timers without asynchronous signal delivery while still observing delayed periodic work through the count returned by read().