os.timerfd_create gives Python programs direct access to Linux timer file descriptors. Instead of relying only on time.sleep(), signals, or a loop that repeatedly checks the clock, an application can create a timer represented by a regular file descriptor. That descriptor becomes readable when the configured deadline expires, so it can be monitored together with sockets, pipes, and other operating-system events.
This model is valuable in servers, monitoring agents, collectors, schedulers, networking tools, and event-driven services. In this guide, you will learn how to create a timerfd, configure one-shot and periodic timers, integrate it with select, read the expiration counter, and avoid portability and cleanup problems.
What is a timer file descriptor?
On Linux, timerfd is a kernel interface that exposes a timer as a file descriptor. When the timer expires, the descriptor becomes readable. Reading eight bytes from it returns an unsigned 64-bit integer indicating how many expirations have occurred since the previous read.
This detail matters because a periodic timer does not simply lose ticks when your process is busy. If three periods pass before your code reads the descriptor, the counter can report 3. That makes it possible to detect lag, missed processing windows, or an overloaded event loop.
The Python os module exposes this feature only on supported platforms and versions. Since timerfd is Linux-specific, production code should check for the function before using it.
import os
if not hasattr(os, "timerfd_create"):
raise RuntimeError("timerfd is not available on this platform")
Creating the descriptor
The creation call receives a clock and optional flags. A monotonic clock is usually the correct choice for durations and intervals because it is not affected when the system’s civil time is changed. A real-time clock is appropriate when the timer must follow wall-clock time.
import os
fd = os.timerfd_create(os.CLOCK_MONOTONIC, os.TFD_CLOEXEC)
try:
print("Timer descriptor:", fd)
finally:
os.close(fd)
TFD_CLOEXEC prevents the descriptor from being inherited unexpectedly by a program launched through an exec-style operation. This is a sensible default in services and command-line tools because leaked descriptors can keep resources alive and complicate debugging.
Configuring a one-shot timer
After creating the descriptor, configure it with os.timerfd_settime. The initial value controls the first expiration. The interval controls repetition. An interval of zero creates a one-shot timer.
import os
import struct
fd = os.timerfd_create(os.CLOCK_MONOTONIC, os.TFD_CLOEXEC)
try:
os.timerfd_settime(fd, initial=2.0, interval=0.0)
raw = os.read(fd, 8)
expirations = struct.unpack("Q", raw)[0]
print("Expirations:", expirations)
finally:
os.close(fd)
The read blocks until the deadline unless the descriptor was created with the nonblocking flag. A one-shot timer will normally return 1. The descriptor remains open after that read and can be programmed again.
Creating a periodic timer
To run a tick every second, set both the initial delay and interval to one second. The kernel then tracks the schedule independently from the execution speed of your Python callback.
import os
import struct
fd = os.timerfd_create(
os.CLOCK_MONOTONIC,
os.TFD_CLOEXEC | os.TFD_NONBLOCK,
)
os.timerfd_settime(fd, initial=1.0, interval=1.0)
try:
received = 0
while received < 5:
try:
count = struct.unpack("Q", os.read(fd, 8))[0]
received += count
print("Ticks:", count)
except BlockingIOError:
continue
finally:
os.close(fd)
The busy loop above is intentionally simple, but it should not be copied into a real service. Nonblocking descriptors are designed to be registered with a polling mechanism so the process sleeps efficiently until an event is ready.
Using timerfd with select or poll
The largest advantage of timerfd is that it participates in the same event loop as network and IPC descriptors. You do not need a dedicated timer thread only to wake another loop.
import os
import select
import struct
fd = os.timerfd_create(
os.CLOCK_MONOTONIC,
os.TFD_CLOEXEC | os.TFD_NONBLOCK,
)
os.timerfd_settime(fd, initial=0.5, interval=0.5)
poller = select.poll()
poller.register(fd, select.POLLIN)
try:
handled = 0
while handled < 3:
for descriptor, mask in poller.poll(2000):
if descriptor == fd and mask & select.POLLIN:
count = struct.unpack("Q", os.read(fd, 8))[0]
handled += count
print("Timer event:", count)
finally:
poller.unregister(fd)
os.close(fd)
In a real application, the same poller could also contain listening sockets, client connections, pipes, or eventfd descriptors. Timer events then become one more event type in a predictable dispatcher.
Monotonic versus real-time clocks
Use CLOCK_MONOTONIC for timeouts, retries, health checks, refresh intervals, and recurring maintenance. It advances steadily even if NTP or an administrator adjusts the system’s calendar time.
Use CLOCK_REALTIME only when the deadline is tied to wall-clock time. Even then, consider how daylight-saving changes, timezone conversion, and clock corrections should affect the operation. For business scheduling, storing the intended date and revalidating it may be safer than trusting one long-lived timer.
Disarming and reprogramming
A timer can be disabled by setting the initial value to zero. You can later configure the same descriptor again.
os.timerfd_settime(fd, initial=0.0, interval=0.0)
Reusing descriptors can simplify resource management, but ownership must be clear. If multiple components reprogram the same timer without coordination, one deadline can silently replace another. Encapsulate the descriptor in a class or keep timer ownership inside one event-loop component.
Error handling and cleanup
Always read exactly eight bytes and decode the value as a 64-bit unsigned integer. Handle BlockingIOError when using TFD_NONBLOCK. Treat a counter larger than 1 as meaningful operational information rather than ignoring it.
Always close the descriptor in finally, a context manager, or a dedicated lifecycle method. Also use TFD_CLOEXEC unless descriptor inheritance is explicitly required.
A timerfd is not persistent. If the process or machine restarts, the timer disappears. Jobs that must survive restarts belong in systemd timers, cron, a durable task queue, or an application scheduler backed by storage.
When timerfd is the right tool
timerfd is a strong choice for Linux daemons, proxies, collectors, high-performance network services, simulators, and programs already built around descriptors. For a small script, time.sleep() is often clearer. For portable asynchronous application code, review asyncio in Python. For precise measurements, see Python perf_counter_ns. You may also find datetime handling and zoneinfo time zones useful.
The official Python os documentation describes the API available in your interpreter. The Linux timerfd_create(2) manual page explains kernel semantics and flags.
Conclusion
os.timerfd_create brings Python into Linux’s descriptor-based timing model. It offers accurate deadlines, natural integration with poll-based loops, and an expiration counter that reveals delayed processing. A reliable implementation checks platform support, prefers a monotonic clock for durations, uses safe flags, reads and decodes the eight-byte counter correctly, and closes the descriptor under every exit path. With those practices, timerfd becomes an efficient building block for infrastructure and event-driven software.







