A periodic Linux timerfd can expire several times before an event loop runs again. The next successful read() does not merely report that the timer fired; it returns an unsigned 64-bit count of expirations accumulated since the previous successful read or since the timer was last configured. Delayed dispatch therefore becomes observable as a count rather than a sequence of queued timer records.

That contract separates timer schedule from consumer execution. A process may be descheduled, an event loop may spend time on other descriptors, or several periods may pass before the timerfd is consumed. The kernel tracks expirations, while application code decides what multiple expirations mean for the work associated with them.

Readiness represents pending expiration state

timerfd_create() creates a timer object exposed through a file descriptor. The selected clock defines the time domain used by the timer. Common choices include CLOCK_MONOTONIC for elapsed-time scheduling and CLOCK_REALTIME for schedules tied to the settable wall clock.

timerfd_settime() arms or disarms the timer. A nonzero it_value sets the first expiration. A nonzero it_interval makes subsequent expirations periodic; a zero interval leaves a one-shot timer.

Once one or more expirations are pending, the descriptor is readable through interfaces such as poll(), select(), and epoll. Readiness remains associated with pending timer state until a successful read consumes the accumulated count.

int fd = timerfd_create(CLOCK_MONOTONIC,
                        TFD_NONBLOCK | TFD_CLOEXEC);

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

timerfd_settime(fd, 0, &spec, NULL);

This example requests a first expiration after one second and a one-second interval thereafter. It does not promise that user-space code runs at each exact boundary. Timer expiration and process scheduling are separate events.

A read returns accumulated expirations

A timerfd read uses an eight-byte buffer and returns a host-order uint64_t. If five expirations occurred before the read, the value can be five. The read consumes that pending expiration count.

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

With no pending expiration, a blocking descriptor waits for the next one. A descriptor created with TFD_NONBLOCK instead fails with EAGAIN. A buffer smaller than eight bytes produces EINVAL.

The count is significant for periodic work. Treating every readiness notification as exactly one elapsed period can lose schedule information when the consumer falls behind. Conversely, executing the associated operation once for every returned expiration is an application policy, not a timerfd requirement. Some systems may coalesce overdue work into one state refresh; others may need to advance a logical counter by the returned amount.

The timerfd preserves expiration count, not execution history. It does not record a timestamp for each missed period, the duration of consumer delay, or the cause of that delay.

Relative and absolute arming encode different boundaries

Without TFD_TIMER_ABSTIME, it_value is interpreted relative to the selected clock at the time of timerfd_settime(). Re-arming a relative timer therefore establishes a new deadline from the call’s current clock value.

With TFD_TIMER_ABSTIME, it_value is an absolute value in the selected clock’s domain. This matters when a schedule must remain anchored to clock positions rather than to the completion time of previous work.

For a periodic timer, the interval continues to define subsequent expiration points after the initial expiration. Delayed reads do not shift those already configured periodic boundaries merely because user space consumed the count late.

This distinction affects drift models. Code that repeatedly performs work and then arms a fresh relative one-shot timer incorporates work and dispatch delay into the next deadline. A periodic timer or explicitly calculated absolute deadlines can keep expiration boundaries tied to a separate schedule. Neither form guarantees prompt user-space execution after an expiration.

Clock selection defines exposure to clock changes

CLOCK_MONOTONIC is not settable and does not jump when the system wall clock is administratively changed. It is therefore suitable for many elapsed-duration deadlines. CLOCK_REALTIME represents wall-clock time and can be changed discontinuously.

An absolute timer based on CLOCK_REALTIME follows that clock’s value. Moving the real-time clock can consequently alter when an absolute deadline is reached. This is a clock-domain property, not an event-loop artifact.

Linux provides TFD_TIMER_CANCEL_ON_SET for a narrower contract. When used together with TFD_TIMER_ABSTIME on CLOCK_REALTIME or CLOCK_REALTIME_ALARM, a discontinuous change to the real-time clock marks the timer so a current or future read can fail with ECANCELED.

That failure exposes clock discontinuity instead of presenting the event as an ordinary expiration count. Applications using civil-time deadlines can then treat administrative clock changes as a separate state transition.

Expiration counts are not backlog capacity

A timerfd count can reveal that periodic boundaries passed while the consumer was delayed, but it does not create storage for the work associated with those boundaries. If each period corresponds to an external sample, network request, or state transition, timerfd does not retain those payloads.

Suppose a one-second timer returns an expiration count of six after a stalled event loop. The kernel has reported six timer expirations. It has not reconstructed six snapshots of application state at those moments. Any mapping from those expirations to domain work depends on state retained elsewhere.

This boundary prevents a common category error: an expiration counter is temporal bookkeeping, not a durable task queue. A queue can preserve per-item payload and ordering according to its own contract; timerfd preserves a count associated with timer expiration.

Shared descriptors also share consumption

After fork(), inherited descriptors refer to the same underlying timer object. Duplicated file descriptors likewise refer to shared timer state. A successful read through one reference consumes the accumulated expiration count seen through the others.

Multiple readers therefore compete rather than subscribe independently. If two event loops monitor duplicated references to one timerfd, they do not each receive a copy of every expiration count.

The object remains active while references remain open. When all file descriptors referring to the timer object are closed, the kernel disarms the timer and releases its resources. TFD_CLOEXEC prevents accidental descriptor retention across a successful execve() when that boundary is desired.

Timer state fits the event loop without becoming a message stream

timerfd places time-based readiness beside sockets, pipes, eventfds, signalfds, and other pollable descriptors. This removes the need to translate the timer into an asynchronous signal solely to wake a descriptor-oriented event loop.

The integration does not turn time into records. Readiness states that expirations are pending, and read() exchanges that pending state for an aggregate count. A slow consumer can observe that multiple periods elapsed, but it still has to apply a domain-specific catch-up rule.

That narrow semantic boundary is the useful property: timer configuration defines expiration points in a chosen clock domain, file-descriptor readiness exposes pending expiration state, and the eight-byte read reports accumulated expirations without claiming that user-space work ran at those times.