Event loops work best when unrelated kinds of work have one common waiting mechanism. Sockets become readable. Pipes become writable. A child-process descriptor or signal descriptor can become ready. Timers are often the awkward exception.
A program can call sleep() or nanosleep(), but that blocks the thread instead of letting it wait for I/O. It can pass a timeout to poll() or epoll_wait(), but one timeout becomes difficult to manage when the program has several independent deadlines. Traditional POSIX timers can deliver signals, which introduces a second asynchronous control path.
Linux provides another option: timerfd. A timer created with timerfd_create() is represented by a file descriptor. When the timer expires, that descriptor becomes readable, so the same poll(), select(), or epoll loop that handles I/O can handle time-based work too.
The mental model is simple:
clock advances
|
timer reaches expiration
|
timer file descriptor becomes readable
|
event loop wakes
|
read(fd) reports how many expirations occurredtimerfd is Linux-specific. It is most useful when a program already uses file-descriptor readiness and wants timers to participate in that same model.
Start with a one-shot monotonic timer
A timerfd is tied to a clock. For elapsed-time deadlines such as “retry in five seconds” or “flush metrics in one minute”, CLOCK_MONOTONIC is usually the right starting point because it is not changed by setting the system’s wall clock.
Creating the descriptor does not start the timer:
#include <sys/timerfd.h>
int timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
if (timer_fd == -1) {
/* handle error */
}TFD_CLOEXEC sets close-on-exec on the new descriptor so it does not accidentally remain open across execve().
To arm the timer, pass an itimerspec to timerfd_settime():
struct itimerspec timer = {
.it_value = {
.tv_sec = 2,
.tv_nsec = 0,
},
.it_interval = {
.tv_sec = 0,
.tv_nsec = 0,
},
};
if (timerfd_settime(timer_fd, 0, &timer, NULL) == -1) {
/* handle error */
}With flags equal to zero, it_value is a relative duration. This timer expires once, roughly two seconds after timerfd_settime() arms it.
it_interval is zero, so the timer does not automatically repeat. Setting both fields of it_value to zero instead would disarm the timer.
Read the expiration count instead of treating readiness as the event itself
When at least one expiration has occurred, the timerfd becomes readable. Reading it returns an unsigned 64-bit count of expirations that have happened since the timer was armed or since the previous successful read.
The read buffer must be at least eight bytes:
#include <stdint.h>
#include <unistd.h>
uint64_t expirations;
ssize_t n = read(timer_fd, &expirations, sizeof expirations);
if (n == -1) {
/* handle error */
} else if (n != sizeof expirations) {
/* unexpected result */
}A successful read consumes the accumulated expiration count. For a one-shot timer, the normal count is 1.
The important point is that readiness is level-triggered state, not a callback. If the event loop notices POLLIN or EPOLLIN but never reads the timerfd, the descriptor remains readable because an expiration is still waiting to be consumed.
This is the same style of reasoning used with other readable descriptors: readiness tells you that an operation can make progress; the read is what consumes the pending information.
Put the timer beside sockets and pipes with poll
The main reason to use timerfd is that the descriptor participates in ordinary readiness APIs.
This complete example creates a periodic timer that first expires after one second and then every second:
#define _GNU_SOURCE
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/timerfd.h>
#include <unistd.h>
int
main(void)
{
int timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
if (timer_fd == -1) {
perror("timerfd_create");
return EXIT_FAILURE;
}
struct itimerspec timer = {
.it_value = {.tv_sec = 1, .tv_nsec = 0},
.it_interval = {.tv_sec = 1, .tv_nsec = 0},
};
if (timerfd_settime(timer_fd, 0, &timer, NULL) == -1) {
perror("timerfd_settime");
close(timer_fd);
return EXIT_FAILURE;
}
struct pollfd item = {
.fd = timer_fd,
.events = POLLIN,
};
for (int handled = 0; handled < 3;) {
int ready = poll(&item, 1, -1);
if (ready == -1) {
perror("poll");
close(timer_fd);
return EXIT_FAILURE;
}
if (item.revents & POLLIN) {
uint64_t expirations;
ssize_t n = read(timer_fd, &expirations, sizeof expirations);
if (n == -1) {
perror("read");
close(timer_fd);
return EXIT_FAILURE;
}
if (n != sizeof expirations) {
fprintf(stderr, "unexpected timerfd read size\n");
close(timer_fd);
return EXIT_FAILURE;
}
printf("timer expirations: %llu\n",
(unsigned long long) expirations);
handled++;
}
}
close(timer_fd);
return EXIT_SUCCESS;
}A real event loop would usually put several descriptors in the pollfd array or register them all with epoll. The timer does not need its own blocking thread just to wait for time to pass.
The example increments handled once per successful read, not once per expiration. That distinction becomes important when the loop is delayed.
Periodic timers can accumulate missed expirations
Suppose a periodic timer expires every 100 milliseconds, but the event loop spends 450 milliseconds doing CPU-heavy work before it reads the timerfd.
The kernel does not need to enqueue four separate eight-byte records. Instead, the next successful read can return a count greater than one.
For example:
uint64_t expirations;
if (read(timer_fd, &expirations, sizeof expirations) != sizeof expirations) {
/* handle error */
}
printf("periods elapsed: %llu\n",
(unsigned long long) expirations);If expirations is 4, four timer periods elapsed before the program consumed the timer state.
What to do with that count is an application policy decision.
A simulation that must advance once for every elapsed interval may need to process four steps. A metrics flush job may prefer to flush once using the newest state and record that it fell behind. A user-interface refresh may simply render the current state once.
Do not automatically run expensive work expirations times without deciding whether catch-up behavior is actually required. If the loop is already overloaded, blindly replaying every missed period can make the backlog worse.
Choose the clock based on what the deadline means
Timer correctness starts with clock choice.
Use CLOCK_MONOTONIC for elapsed runtime
CLOCK_MONOTONIC is suitable for durations measured while the system is running. It cannot be set through normal wall-clock adjustments, so changing the date or synchronizing the real-time clock does not move a monotonic deadline.
A retry backoff, request timeout, or periodic maintenance interval usually wants this behavior.
One boundary matters: on Linux, CLOCK_MONOTONIC does not count time while the system is suspended.
Use CLOCK_BOOTTIME when suspend time should count
CLOCK_BOOTTIME is monotonic like CLOCK_MONOTONIC, but it includes time spent suspended.
That matters for a requirement such as “expire this cached lease ten minutes after it was created even if the laptop sleeps for eight of those minutes.”
CLOCK_BOOTTIME does not by itself mean the timer wakes a suspended system. The _ALARM clock variants are for timers that can wake the system and require appropriate privilege.
Use CLOCK_REALTIME for wall-clock deadlines carefully
CLOCK_REALTIME represents wall-clock time and can change discontinuously when an administrator or time-management service changes the clock.
That makes it appropriate when the requirement is tied to a civil-time value, but less appropriate for elapsed durations.
For most internal timeouts, prefer a monotonic clock. Use real time when the deadline genuinely means “at this wall-clock instant.”
Use absolute deadlines when repeated work must not drift
A relative one-shot timer is convenient:
work finishes -> arm timer for 1 second -> wait -> do work againBut if each iteration takes time, scheduling the next delay after the work finishes shifts the phase.
Suppose the intended cadence is one task every second and the task itself takes 200 milliseconds:
relative-after-work:
0.0 run
1.2 run
2.4 run
3.6 runThat may be correct if the requirement is “wait one second after each completion.” It is wrong if the requirement is “run on a one-second schedule.”
For fixed deadlines, TFD_TIMER_ABSTIME interprets it_value as an absolute value on the timer’s clock:
#include <time.h>
struct timespec now;
if (clock_gettime(CLOCK_MONOTONIC, &now) == -1) {
/* handle error */
}
struct itimerspec timer = {
.it_value = {
.tv_sec = now.tv_sec + 5,
.tv_nsec = now.tv_nsec,
},
.it_interval = {
.tv_sec = 0,
.tv_nsec = 0,
},
};
if (timerfd_settime(
timer_fd,
TFD_TIMER_ABSTIME,
&timer,
NULL) == -1) {
/* handle error */
}This sets a one-shot expiration five seconds beyond the sampled monotonic time.
For recurring fixed-rate work, you can also set a nonzero interval. The key mental model is that an absolute initial expiration anchors the schedule to a clock value rather than to the moment the arming call happens.
Absolute deadlines are especially useful when the application computes a sequence such as start + n * period. If one iteration finishes late, the next deadline still comes from the intended schedule instead of from the late completion time.
Nonblocking mode changes an early read into EAGAIN
By default, reading a timerfd before it has expired blocks until an expiration occurs.
Event loops often use nonblocking descriptors. Pass TFD_NONBLOCK when creating the timer:
int timer_fd = timerfd_create(
CLOCK_MONOTONIC,
TFD_CLOEXEC | TFD_NONBLOCK);If no expiration is pending, read() then fails with EAGAIN instead of blocking.
With a readiness loop, this is useful defensive behavior: after poll() or epoll reports readability, the code can drain the timerfd without risking an unexpected indefinite block if state changed between readiness handling steps.
A simple poll() loop with only one descriptor does not require nonblocking mode. Use it when it matches the broader event-loop design rather than treating it as mandatory.
Re-arm and disarm the same descriptor
A timerfd is a timer object, not a one-use notification.
Calling timerfd_settime() again replaces the current timer setting. This makes one descriptor suitable for mutable deadlines such as an idle timeout.
For example, an application can keep a one-shot timerfd for “disconnect after 30 seconds without activity” and re-arm it whenever activity arrives.
To disarm a timer:
struct itimerspec stop = {0};
if (timerfd_settime(timer_fd, 0, &stop, NULL) == -1) {
/* handle error */
}A zero it_value disarms the timer regardless of the interval field.
If you need to inspect the current setting, timerfd_gettime() returns the remaining time until the next expiration and the configured interval. The remaining it_value is reported as a relative duration even if the timer was originally armed with an absolute deadline.
Understand what timerfd does not guarantee
Using a timerfd changes how a program receives timer notifications. It does not turn Linux into a hard real-time scheduler.
When a timer expires, the descriptor becomes readable. Your thread still needs to be scheduled, return from whatever work it is doing, and process the readiness event.
Therefore:
- an expiration time is not a guarantee that user-space code runs at that exact instant;
- a busy event loop may observe multiple accumulated expirations;
- scheduler load and long-running handlers can add latency;
- clock resolution and timer implementation affect when the kernel can deliver the expiration.
Use the expiration count to detect that periods were missed, but design latency requirements separately. Applications with hard real-time requirements need stronger scheduling and system-level guarantees than timerfd alone provides.
Avoid common timerfd mistakes
Treating CLOCK_REALTIME like an elapsed-time clock
If an operation must time out after a duration, a wall-clock adjustment should usually not extend or shorten that duration. Use a monotonic clock for that case.
Ignoring expiration counts
A periodic timer can return a value greater than one. Code that assumes every read means exactly one period can silently lose information about how far behind the event loop became.
Forgetting to read after readiness
The descriptor remains readable while expirations are pending. With level-triggered readiness, ignoring the read can cause the event loop to wake repeatedly for the same pending timer state.
Doing unlimited catch-up work
If ten periods elapsed, running the same expensive task ten times may be worse than processing current state once. Decide whether each elapsed period represents indispensable work or merely a missed opportunity to refresh.
Assuming periodic timers prevent application drift
A kernel periodic timer keeps its own expiration schedule, but your application can still fall behind in processing those expirations. If the actual operation has strict phase requirements, reason explicitly about deadlines and the returned expiration count.
When timerfd is the right tool
Use timerfd when:
- the program runs on Linux;
- it already waits on file descriptors;
- timer events should share a
poll,select, orepollloop with I/O; - you need one-shot or periodic timers with explicit clock semantics;
- accumulated expiration counts are useful for detecting missed periods.
A different tool may be simpler when none of those conditions apply.
For a small sequential program, nanosleep() may communicate intent more clearly. A portable library should usually use its cross-platform timer abstraction. A framework with an established event loop generally already has a timer API that handles descriptor registration and deadline bookkeeping internally.
Timerfd is valuable not because every timer should be a file descriptor, but because Linux event-driven programs often become easier to reason about when I/O, signals, process events, and time all enter through one readiness mechanism.
Conclusion
timerfd turns timer expiration into ordinary file-descriptor state.
Create the descriptor with the clock that matches the meaning of the deadline, arm it with timerfd_settime(), wait for readability alongside other I/O, and read the 64-bit expiration count when it fires. Use CLOCK_MONOTONIC for most elapsed-time deadlines, CLOCK_BOOTTIME when suspend time must count, and wall-clock timers only when the deadline is genuinely tied to real time.
Most importantly, treat the expiration count as information about elapsed periods, not as an instruction to replay work blindly. That distinction keeps periodic timers correct even when the event loop occasionally falls behind.