Over-Engineering Sleep

2025-08-24

A while back, this implementation of a cross-platform sleep for GitHub Actions Runners caught my eye, and I wanted to see what it would take to re-implement sleep from the ground up, while trying to be as cross-platform as possible.

A sleeping computer monitor.

Analysis

The motivation of this implementation seems to be around not relying on the sleep utility, despite this being part of the POSIX standard. Instead, it uses a pure bash solution with the SECONDS bash builtin (manpage reference).

Looking at the source of bash reveals that this builtin is implemented using the gettimeofday call (assign_seconds, get_seconds). If we rely on gettimeofday anyway, why not rely on sleep?

Indeed, Matthew Lugg points out that this approach is not only more complex, but buggy and is vulnerable to cases where the runner is highly loaded, the system clock is adjusted and so on. A straightforward fix for the deadlock was eventually merged - using a less-than comparator instead of equality.

But we are engineers, so let’s over-engineer this whole thing by building our own sleep utility from the ground up.

Sticking with bash

To start with, let’s sidestep the use of a realtime clock (through gettimeofday calls) and instead use the monotonic clock provided by the bash builtin BASH_MONOSECONDS. Introduced in bash 5.3 (release notes), new and not universally available. Such a solution would look like:

#!/usr/bin/env bash

duration=$1
start=$BASH_MONOSECONDS
end=$((start + duration))

while [[ $BASH_MONOSECONDS -lt end ]]; do
  :
done

Like the earlier implementation, this is a busy-wait loop. Let’s do better and take advantage of some alternatives.

Zig-a-zig-ah

Zig is a handy language for doing this kind of work - fast compilation and easy to do cross-platform work, perfect for building our own portable sleep.

First, let’s avoid the busy-wait loop by going with an asynchronous approach. In Linux, we have timefd_create, kqueue in BSD and CreateWaitableTimer in Windows. Let’s start with the kqueue implementation.

Kqueue

const std = @import("std");

fn sleepKqueue(ms: u64) !void {
    const posix = std.posix;

    const kq = try posix.kqueue();
    defer posix.close(kq);

    const kev = posix.Kevent{
        .ident = 0,
        .filter = posix.system.EVFILT.TIMER,
        .flags = posix.system.EV.ADD | posix.system.EV.ONESHOT,
        .fflags = 0,
        .data = @as(isize, @intCast(ms)),
        .udata = @intFromPtr(@as(?*anyopaque, null)),
    };

    _ = try posix.kevent(kq, &[_]posix.Kevent{kev}, &[_]posix.Kevent{}, null);

    var out: [1]posix.Kevent = undefined;
    const n = try posix.kevent(kq, &[_]posix.Kevent{}, &out, null);
    if (n != 1) return error.TimerWaitFailed;
}

kqueue is the BSD equivalent of epoll in Linux and provides a scalable way to monitor multiple file descriptors. In our case, we’re using it to wait for a timer event. With a bit of CLI wrapping:

$ time ./ksleep 2

real	0m2.019s
user	0m0.003s
sys	    0m0.008s

Contrast with the earlier bash implementation:

$ time /tmp/badsleep.sh 2

real	0m1.875s
user	0m1.854s
sys	    0m0.016s

The ksleep implementation is not only more efficient in CPU usage, but also more accurate in timing - we can specify millisecond precision:

$ time ./ksleep 2.5

real	0m2.523s
user	0m0.003s
sys	    0m0.010s

Next, lets look at the Linux implementation with timerfd:

Timerfd

fn sleepTimerFd(seconds: f64) !void {
    const sys = std.os.linux;
    const flags = sys.TFD{ .CLOEXEC = true };

    const rawfd = sys.timerfd_create(sys.timerfd_clockid_t.MONOTONIC, flags);
    const fd = @as(i32, @intCast(rawfd));
    if (fd < 0) return error.TimerFdCreateFailed;
    defer std.posix.close(fd);

    const sec: i64 = @intFromFloat(@floor(seconds));
    const nsec: i64 = @intFromFloat((seconds - @as(f64, @floatFromInt(sec))) * 1_000_000_000);

    var new_value = sys.itimerspec{
        .it_interval = .{ .sec = 0, .nsec = 0 },
        .it_value = .{ .sec = sec, .nsec = nsec },
    };

    const no_flags = sys.TFD.TIMER{};

    if (sys.timerfd_settime(fd, no_flags, &new_value, null) < 0)
        return error.TimerSetFailed;

    var buf: [8]u8 = undefined;
    const n = try std.posix.read(fd, &buf);
    if (n != 8) return error.TimerReadFailed;
}

As before, this implementation is efficient and accurate:

root@13738b47c4e3:/ksleep# time ./ksleep 2.5

real	0m2.507s
user	0m0.001s
sys	    0m0.002s

timerfd_create has been available since Linux 2.6.25, so it is widely supported.

CreateWaitableTimer

I don’t have a Windows machine, so this exercise is left up to the reader.

Wrap Up

It’s not that hard to build a pretty modern sleep utility without relying on the existing sleep command. The example I’ve shown here is not exactly production ready, but it builds statically and remains small, less than 128 KB. No need for horrible bash busy-wait loops.

The final code looks like this:

// MIT License

// Copyright (c) 2025 Matthew Blewitt

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

const std = @import("std");
const builtin = @import("builtin");

pub fn main() !void {
    var buf: [512]u8 = undefined;
    var fba = std.heap.FixedBufferAllocator.init(&buf);
    const allocator = fba.allocator();
    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);

    if (args.len != 2) {
        std.debug.print("Usage: {s} <seconds>\n", .{args[0]});
        std.process.exit(1);
    }

    const seconds = std.fmt.parseFloat(f64, args[1]) catch {
        std.debug.print("Invalid number: {s}\n", .{args[1]});
        std.process.exit(1);
    };

    if (seconds < 0) {
        std.debug.print("Duration must be >= 0\n", .{});
        std.process.exit(1);
    }

    const millis = @as(u64, @intFromFloat(seconds * 1000.0));
    switch (builtin.os.tag) {
        .linux => try sleepTimerFd(seconds),
        .macos, .ios, .freebsd, .netbsd, .openbsd, .dragonfly => try sleepKqueue(millis),
        else => {
            std.debug.print("Unsupported OS: {s}\n", .{builtin.os.tag});
            std.process.exit(1);
        },
    }
}

fn sleepKqueue(ms: u64) !void {
    const posix = std.posix;

    const kq = try posix.kqueue();
    defer posix.close(kq);

    const kev = posix.Kevent{
        .ident = 0,
        .filter = posix.system.EVFILT.TIMER,
        .flags = posix.system.EV.ADD | posix.system.EV.ONESHOT,
        .fflags = 0,
        .data = @as(isize, @intCast(ms)),
        .udata = @intFromPtr(@as(?*anyopaque, null)),
    };

    _ = try posix.kevent(kq, &[_]posix.Kevent{kev}, &[_]posix.Kevent{}, null);

    var out: [1]posix.Kevent = undefined;
    const n = try posix.kevent(kq, &[_]posix.Kevent{}, &out, null);
    if (n != 1) return error.TimerWaitFailed;
}

fn sleepTimerFd(seconds: f64) !void {
    const sys = std.os.linux;
    const flags = sys.TFD{ .CLOEXEC = true };

    const rawfd = sys.timerfd_create(sys.timerfd_clockid_t.MONOTONIC, flags);
    const fd = @as(i32, @intCast(rawfd));
    if (fd < 0) return error.TimerFdCreateFailed;
    defer std.posix.close(fd);

    const sec: i64 = @intFromFloat(@floor(seconds));
    const nsec: i64 = @intFromFloat((seconds - @as(f64, @floatFromInt(sec))) * 1_000_000_000);

    var new_value = sys.itimerspec{
        .it_interval = .{ .sec = 0, .nsec = 0 },
        .it_value = .{ .sec = sec, .nsec = nsec },
    };

    const no_flags = sys.TFD.TIMER{};

    if (sys.timerfd_settime(fd, no_flags, &new_value, null) < 0)
        return error.TimerSetFailed;

    var buf: [8]u8 = undefined;
    const n = try std.posix.read(fd, &buf);
    if (n != 8) return error.TimerReadFailed;
}