Timer Slack and Deferrable Timers

The kernel has two distinct mechanisms for trading a little timing inaccuracy for power savings, and they are easy to conflate because both make timers fire “late.” Timer slack is a per-thread allowance — a value in nanoseconds (default 50 µs) that the kernel adds to a deadline so it can batch nearby wakeups into one, letting a CPU sleep through a cluster of would-be interrupts; it applies to the high-resolution-timer _range APIs and to blocking syscalls like select/poll/nanosleep. Deferrable timers are a flag (TIMER_DEFERRABLE) on the coarse timer wheel: a deferrable timer never wakes an idle CPU just to service it — it fires only when that CPU next wakes for some other reason, so an idle core can stay in a NOHZ state arbitrarily longer. Both are about not generating a wakeup; the difference is that slack bounds the lateness to a small constant, while a deferrable timer can be late indefinitely. This note is pinned to Linux 6.12 LTS (released 2024-11-17), verified against the raw v6.12 source.


Why Inaccuracy Buys Power

Every timer interrupt has a cost beyond the handful of cycles to run the callback: it drags the CPU out of whatever low-power idle state it was in, and a CPU that is woken frequently can never reach the deepest C-states (where caches and clocks are progressively powered down). On a laptop or phone, a swarm of independent timers each firing a few hundred microseconds apart can keep a core awake continuously even though it does almost nothing. The cure is wakeup coalescing: if two timers want to fire close together, wake the CPU once and service both. To do that legally the kernel needs permission to be a little late — and that permission is exactly what timer slack and deferrable timers grant, in two different shapes.

The principle generalizes a NOHZ insight: the enemy of idle power is the number of wakeups, not the work per wakeup. Anything that lets the kernel skip or merge a wakeup is a power win, paid for in timing precision.


Mental Model

flowchart TB
  subgraph SLACK["Timer slack (bounded lateness)"]
    direction TB
    A["deadline D"] --> R["range [D, D+slack]"]
    R --> M["fire together with any<br/>other timer already due in window"]
  end
  subgraph DEFER["Deferrable timer (unbounded lateness)"]
    direction TB
    DT["TIMER_DEFERRABLE timer<br/>on BASE_DEF wheel"] --> IDLE{"CPU idle?"}
    IDLE -->|yes| SKIP["do NOT wake CPU<br/>stay in NOHZ/deep-idle"]
    IDLE -->|"woken for<br/>other reason"| RUN["service now"]
  end

The two mechanisms side by side. What it shows (left): timer slack widens a single deadline D into a small window [D, D+slack]; the timer may fire anywhere in that window, so the kernel can align it with another wakeup already happening inside it — the lateness is bounded by slack. (right): a deferrable timer lives on a separate wheel base that is simply ignored by the idle/NOHZ path, so it produces no wakeup at all on an idle CPU and is serviced opportunistically when the CPU wakes for something else — the lateness is unbounded. The insight to take: slack is “fire roughly here, within a budget”; deferrable is “fire whenever you happen to be awake.” Use slack when lateness must stay small; use deferrable for genuinely non-urgent housekeeping that can wait minutes.


Timer Slack

The per-thread value and its default

Every task carries two slack values, set up in the task structure: timer_slack_ns (the current value, used now) and default_timer_slack_ns (the value to restore to). The ancestor of all processes, the init task, seeds them. From init/init_task.c:

.timer_slack_ns = 50000, /* 50 usec default slack */

So the system-wide default is 50,000 ns = 50 µs, confirmed by the man page: “The timer slack values of init(1) (PID 1), the ancestor of all processes, are 50,000 nanoseconds (50 microseconds)” (PR_SET_TIMERSLACK(2const)). A new task inherits the parent’s current slack as its default — from kernel/fork.c:

p->default_timer_slack_ns = current->timer_slack_ns;

That is the only place default_timer_slack_ns is propagated, so changing a process’s current slack and then forking children gives the children that value as their floor.

Reading and writing it: prctl(PR_SET_TIMERSLACK)

A thread adjusts its own slack with prctl(2). From kernel/sys.c:

case PR_GET_TIMERSLACK:
        if (current->timer_slack_ns > ULONG_MAX)
                error = ULONG_MAX;
        else
                error = current->timer_slack_ns;
        break;
case PR_SET_TIMERSLACK:
        if (rt_or_dl_task_policy(current))
                break;                                   // (1)
        if (arg2 <= 0)
                current->timer_slack_ns =
                                current->default_timer_slack_ns; // (2)
        else
                current->timer_slack_ns = arg2;          // (3)
        break;

Three behaviors worth reading carefully. (1) If the calling thread runs under a real-time or deadline scheduling policy (SCHED_FIFO, SCHED_RR, SCHED_DEADLINE), the PR_SET_TIMERSLACK is silently ignored — slack would corrupt the timing guarantees those policies exist to provide. The man page states it plainly: “Timer slack is not applied to threads that are scheduled under a real-time scheduling policy.” (2) Passing arg2 <= 0 (notably zero) resets the current slack to the thread’s saved default rather than setting it to zero — a deliberate “undo my change” semantic. (3) Any positive value becomes the new current slack. The value is per-thread, not per-process, despite the PR_ prefix’s process connotation — each thread has its own timer_slack_ns. One can also read or write another process’s value via /proc/<pid>/timerslack_ns (subject to CAP_SYS_NICE).

Uncertain

Verify: that the /proc/<pid>/timerslack_ns file exists in v6.12 and is gated by CAP_SYS_NICE / ptrace-access checks. Reason: this is stated from general knowledge of the procfs interface added for Android; it was not fetched from the v6.12 fs/proc/base.c source during this task. To resolve: grep fs/proc/base.c in v6.12 for timerslack_ns. uncertain

How slack is actually consumed

A bare slack value does nothing on its own; a code path has to spend it by widening a timer into a range. The two consumers are the hrtimer _range APIs and blocking syscalls.

On the hrtimer side, the entry point is [[High-Resolution Timers and hrtimers|hrtimer_start_range_ns()]]: its delta_ns argument is the slack. Recall a struct hrtimer carries two expiry fields (include/linux/hrtimer_types.h): _softexpires (earliest allowed) and node.expires (latest allowed). hrtimer_set_expires_range_ns() sets _softexpires = tim and node.expires = tim + delta_ns, and the expiry loop in __hrtimer_run_queues() compares “now” against _softexpires, so the timer becomes eligible at the earliest edge of the window and the hardware is programmed for the latest edge of whichever timer is the true constraint. That gap is precisely the room the kernel uses to coalesce: a timer can be pulled forward to ride along with an unrelated interrupt already firing inside its window, adding no new wakeup. (The source comment: “the immediate goal for using the softexpires is minimizing wakeups.”)

The syscall side is where a process’s timer_slack_ns enters. select/pselect/poll/ppoll route their timeout through select_estimate_accuracy() in fs/select.c:

u64 select_estimate_accuracy(struct timespec64 *tv)
{
        u64 slack = current->timer_slack_ns;
        if (slack == 0)
                return 0;
        ktime_get_ts64(&now);
        now = timespec64_sub(*tv, now);
        ret = __estimate_accuracy(&now);    // ~0.1% of the timeout, capped 100 ms
        if (ret < slack)
                return slack;               // never below the thread's slack
        return ret;
}

The returned accuracy is then handed to schedule_hrtimeout_range() as its slack argument, so a poll() with a 1-second timeout is allowed roughly 1 ms (0.1%) of lateness — far more than the 50 µs floor — and the kernel happily merges that wakeup with anything nearby. __estimate_accuracy() even gives lower-priority (“nice”) tasks 5× more slack (divfactor / 5), since a background task’s wakeups matter less. Per the man page, the full set of slack-affected syscalls is select, pselect, poll, ppoll, epoll_wait, epoll_pwait, clock_nanosleep, nanosleep, and futex.

A crucial invariant: slack only ever makes a timer fire late, never early. The man page is explicit: “timer expirations for the thread may be up to the specified number of nanoseconds late (but will never expire early).” That is what makes slack safe to apply blindly — a sleep of at least N nanoseconds is still honored.


Deferrable Timers

What the flag means

TIMER_DEFERRABLE is a flag on a timer-wheel timer (struct timer_list), not an hrtimer. Its contract, from the header comment in include/linux/timer.h:

“A deferrable timer will work normally when the system is busy, but will not cause a CPU to come out of idle just to service it; instead, the timer will be serviced when the CPU eventually wakes up with a subsequent non-deferrable timer.”

The flag bit is TIMER_DEFERRABLE = 0x00080000 (include/linux/timer.h). It composes with the other wheel flags (TIMER_PINNED, TIMER_IRQSAFE). The convenience initializers are DEFINE_TIMER_DEFERRABLE / timer_setup() with the flag, but the essence is a single bit in timer->flags.

The separate wheel base

The mechanism is structural: deferrable timers live on a different per-CPU timer base that the idle path simply does not consult. When CONFIG_NO_HZ_COMMON is configured, each CPU has three bases instead of one (kernel/time/timer.c):

# define NR_BASES   3
# define BASE_LOCAL  0
# define BASE_GLOBAL 1
# define BASE_DEF    2

The source comment explains the split: “If NOHZ is configured we allocate two wheels so we have a separate storage for the deferrable timers.” When a timer is enqueued, get_target_base() routes it:

if (IS_ENABLED(CONFIG_NO_HZ_COMMON) && (tflags & TIMER_DEFERRABLE))
        base = this_cpu_ptr(&timer_bases[BASE_DEF]);

So every deferrable timer goes to BASE_DEF, and the non-deferrable timers go to BASE_LOCAL/BASE_GLOBAL. The whole trick is that the NOHZ idle code computes the next wakeup deadline from the LOCAL and GLOBAL bases onlyBASE_DEF is excluded from the “when do I have to wake up?” calculation. A deferrable timer therefore never extends the time the CPU must stay awake.

Why no IPI is sent

There is a second, complementary half: when a timer becomes the new earliest in its base, enqueue_timer() normally calls trigger_dyntick_cpu() to kick a possibly-idle remote CPU so it can re-evaluate its sleep length. For deferrable timers that kick is skipped outright (kernel/time/timer.c):

static void trigger_dyntick_cpu(struct timer_base *base, struct timer_list *timer)
{
        /*
         * Deferrable timers do not prevent the CPU from entering dynticks and
         * are not taken into account on the idle/nohz_full path. An IPI when a
         * new deferrable timer is enqueued will wake up the remote CPU but
         * nothing will be done with the deferrable timer base. Therefore skip
         * the remote IPI for deferrable timers completely.
         */
        if (!is_timers_nohz_active() || timer->flags & TIMER_DEFERRABLE)
                return;
        ...
}

The comment is the whole story: sending an inter-processor interrupt (IPI) to wake an idle CPU for a deferrable timer would be self-defeating — the CPU would wake, look at its LOCAL/GLOBAL bases, find nothing due, and go back to sleep, having burned a wakeup. So the IPI is suppressed and the deferrable timer simply waits in BASE_DEF until that CPU wakes for a real reason and drains all its bases.

The consequence: unbounded lateness

Because nothing ever wakes a CPU for a deferrable timer, on a CPU that goes idle and stays idle, a deferrable timer can be arbitrarily late — seconds or longer. That is by design and is the right trade for non-urgent periodic housekeeping: cache reclaim throttling, statistics that can be stale, vmstat folding, slab shrinker bookkeeping. None of those need to fire on time; they need to fire eventually, and not at the cost of keeping a core awake.


Putting Them Side by Side

PropertyTimer slackDeferrable timer
Timer typehrtimer _range / blocking syscallstimer wheel (timer_list)
Latenessbounded by the slack valueunbounded on an idle CPU
Granularityper-thread (timer_slack_ns) or per-callper-timer flag (TIMER_DEFERRABLE)
Wakes an idle CPU?yes, but coalesced with nearby wakeupsno — never on its own
Set byprctl(PR_SET_TIMERSLACK), _range APIsTIMER_DEFERRABLE at timer init
Honors real-time tasksdisabled for RT/DL policiesn/a (kernel-internal flag)

The table is a quick index, but the operative distinction is the one in the mental-model caption: slack says “fire within a small budget of the deadline so I can merge wakeups”; deferrable says “I do not care when you fire as long as you do not wake me.” Reach for slack when the lateness must stay small and the timer is precision-class (an hrtimer, a poll timeout); reach for TIMER_DEFERRABLE only for genuinely non-urgent kernel housekeeping where minutes of lateness are acceptable.


Failure Modes and Common Misunderstandings

  • Setting slack on a real-time thread does nothing. PR_SET_TIMERSLACK returns success but the rt_or_dl_task_policy(current) guard in sys.c skips the assignment. A real-time program that “set slack to 0 for determinism” was already getting zero-effect slack; the determinism comes from the RT policy, not the slack call.
  • prctl(PR_SET_TIMERSLACK, 0) does not mean “no slack.” A non-positive argument resets to the default (50 µs by default), it does not zero the slack. To truly minimize slack a non-RT thread would set 1 ns, not 0.
  • Deferrable timers are not for anything time-sensitive. Because lateness is unbounded, a deferrable timer used for, say, a connection keepalive can fire seconds late on an idle machine and break the protocol. The flag is for housekeeping only.
  • Deferrable + pinned interactions. A deferrable timer still has a CPU base; the comment in trigger_dyntick_cpu() warns that the only legitimate cases for waking an idle base are pinned timers or nohz_full CPUs — deferrable is explicitly excluded from both, so do not expect a deferrable timer on an isolated nohz_full CPU to be serviced promptly. See Full Dynticks and NOHZ_FULL.
  • Slack stacks with __estimate_accuracy. For poll/select the effective slack is max(timer_slack_ns, ~0.1% of timeout), so for long timeouts the per-thread floor is irrelevant — lowering timer_slack_ns will not tighten a 10-second poll.

Production Notes

Timer slack is a real power lever on mobile and embedded systems. Android famously raises the timer slack of background apps far above 50 µs (historically into the millisecond range) to aggressively coalesce their wakeups and extend battery life, restoring tight slack when an app is foregrounded — which is why the writable /proc/<pid>/timerslack_ns interface exists. On servers the default 50 µs is usually left alone; latency-sensitive userspace (trading, audio) instead relies on a real-time scheduling policy, which disables slack entirely. Deferrable timers are almost invisible to userspace because they are a kernel-internal optimization, but their effect shows up in idle-power measurements: converting a frequently-armed housekeeping timer_list to TIMER_DEFERRABLE can measurably increase residency in deep C-states on an otherwise-idle box. Both mechanisms are best understood as instruments serving the same goal as NOHZ — minimizing the number of times a sleeping CPU is forced awake — and they are most effective in combination with it. To observe coalescing in practice, the timer_start/hrtimer_start tracepoints (see Linux Tracing and Observability MOC) show the programmed vs. effective expiry, and powertop attributes wakeups to specific timers.


See Also