Skip to content

Time and Timers

Programs often need to work with time.

Programs often need to work with time.

A program may need to measure how long something takes. It may need to wait for a short duration. It may need to store timestamps in logs. It may need to compare two moments.

Zig’s standard library provides time utilities in:

std.time

For beginners, the most important use is measuring elapsed time.

Getting a Timestamp

A timestamp is a number that represents a point in time.

One simple function is:

std.time.nanoTimestamp()

It returns a timestamp in nanoseconds.

A nanosecond is one billionth of a second.

const std = @import("std");

pub fn main() void {
    const now = std.time.nanoTimestamp();

    std.debug.print("now = {}\n", .{now});
}

The exact number is not usually meaningful by itself. It is useful when you compare it with another timestamp.

Measuring Elapsed Time

To measure elapsed time, take one timestamp before the work and one timestamp after the work.

const std = @import("std");

pub fn main() void {
    const start = std.time.nanoTimestamp();

    var sum: u64 = 0;
    for (0..1_000_000) |i| {
        sum += i;
    }

    const end = std.time.nanoTimestamp();

    std.debug.print("sum = {}\n", .{sum});
    std.debug.print("elapsed ns = {}\n", .{end - start});
}

This pattern is common:

const start = std.time.nanoTimestamp();

// work

const end = std.time.nanoTimestamp();
const elapsed = end - start;

The result is the elapsed time in nanoseconds.

Converting Units

Nanoseconds are precise, but they can be hard to read.

Zig defines useful constants such as:

std.time.ns_per_us
std.time.ns_per_ms
std.time.ns_per_s

These mean:

ConstantMeaning
std.time.ns_per_usnanoseconds per microsecond
std.time.ns_per_msnanoseconds per millisecond
std.time.ns_per_snanoseconds per second

Example:

const std = @import("std");

pub fn main() void {
    const start = std.time.nanoTimestamp();

    var sum: u64 = 0;
    for (0..10_000_000) |i| {
        sum += i;
    }

    const end = std.time.nanoTimestamp();
    const elapsed_ns = end - start;

    const elapsed_ms = @divTrunc(elapsed_ns, std.time.ns_per_ms);

    std.debug.print("sum = {}\n", .{sum});
    std.debug.print("elapsed ms = {}\n", .{elapsed_ms});
}

This converts nanoseconds to milliseconds using integer division.

Integer Division Loses the Remainder

This line:

const elapsed_ms = @divTrunc(elapsed_ns, std.time.ns_per_ms);

drops the fractional part.

For example, if the elapsed time is:

1.7 ms

integer division gives:

1 ms

That may be good enough for simple reporting.

For more detail, print both milliseconds and remaining nanoseconds, or use floating point conversion carefully.

const elapsed_ms = @as(f64, @floatFromInt(elapsed_ns)) /
    @as(f64, @floatFromInt(std.time.ns_per_ms));

std.debug.print("elapsed ms = {d:.3}\n", .{elapsed_ms});

This prints a decimal number with three digits after the decimal point.

Sleeping

Sleeping means pausing the current thread for some duration.

A common function is:

std.time.sleep()

It receives a duration in nanoseconds.

const std = @import("std");

pub fn main() void {
    std.debug.print("before sleep\n", .{});

    std.time.sleep(1 * std.time.ns_per_s);

    std.debug.print("after sleep\n", .{});
}

This pauses for about one second.

Use constants to make durations readable:

std.time.sleep(500 * std.time.ns_per_ms);

This sleeps for about 500 milliseconds.

Sleep Is Not Exact

A sleep duration is a request to the operating system.

This:

std.time.sleep(10 * std.time.ns_per_ms);

means “do not run this thread for at least about 10 milliseconds.”

It does not guarantee exact timing.

The operating system scheduler, system load, timer resolution, and platform behavior all affect when the thread resumes.

For ordinary delays, sleep is fine.

For precise benchmarking, do not use sleep as a timing mechanism.

Timers

For measuring elapsed time, Zig also provides timer utilities.

The exact timer APIs may vary across versions, but the idea is simple:

Create a timer.

Do work.

Ask the timer how much time elapsed.

Conceptually:

var timer = try std.time.Timer.start();

// work

const elapsed = timer.read();

A timer is often clearer than manually storing two timestamps.

Example shape:

const std = @import("std");

pub fn main() !void {
    var timer = try std.time.Timer.start();

    var sum: u64 = 0;
    for (0..10_000_000) |i| {
        sum += i;
    }

    const elapsed = timer.read();

    std.debug.print("sum = {}\n", .{sum});
    std.debug.print("elapsed ns = {}\n", .{elapsed});
}

Use this pattern when available in your installed Zig version.

Wall Time vs Monotonic Time

There are two different ideas of time.

Wall time is calendar time. It answers questions like:

What date is it?

What time is it now?

What timestamp should I put in a log?

Monotonic time is measuring time. It answers questions like:

How long did this operation take?

Has this timeout expired?

For measuring durations, prefer monotonic time when possible.

Wall time can jump forward or backward because of clock synchronization, manual clock changes, daylight saving changes, or system time corrections.

Monotonic time is designed for elapsed-time measurement.

Timing Small Code Is Hard

This kind of benchmark is tempting:

const start = std.time.nanoTimestamp();

doSomething();

const end = std.time.nanoTimestamp();
std.debug.print("{}\n", .{end - start});

It is useful for rough checks, but it is not a serious benchmark.

Small timings are noisy.

The compiler may optimize away unused work.

The CPU may change speed.

The operating system may interrupt your process.

The first run may behave differently from later runs.

For simple learning, manual timing is fine. For serious performance work, run many iterations, prevent unwanted optimization, use release builds, and compare results carefully.

A Simple Repeated Timing Program

This example runs work many times and measures the total time.

const std = @import("std");

fn work() u64 {
    var sum: u64 = 0;

    for (0..1_000_000) |i| {
        sum += i;
    }

    return sum;
}

pub fn main() void {
    const start = std.time.nanoTimestamp();

    var result: u64 = 0;
    for (0..100) |_| {
        result ^= work();
    }

    const end = std.time.nanoTimestamp();
    const elapsed_ns = end - start;

    const elapsed_ms = @as(f64, @floatFromInt(elapsed_ns)) /
        @as(f64, @floatFromInt(std.time.ns_per_ms));

    std.debug.print("result = {}\n", .{result});
    std.debug.print("elapsed ms = {d:.3}\n", .{elapsed_ms});
}

The variable result matters.

Without it, the compiler might decide that the work has no visible effect and remove some of it in optimized builds.

This example still does not replace a real benchmark, but it teaches the right suspicion: timing code must be written carefully.

Timeouts

A timeout means “stop waiting after this much time.”

For example, a network program might wait for data, but only for 5 seconds.

The general idea is:

const deadline = start_time + timeout;

// later
if (now >= deadline) {
    return error.Timeout;
}

A simple loop can use this pattern:

const std = @import("std");

pub fn main() !void {
    const timeout_ns = 2 * std.time.ns_per_s;
    const start = std.time.nanoTimestamp();

    while (true) {
        const now = std.time.nanoTimestamp();

        if (now - start >= timeout_ns) {
            std.debug.print("timeout\n", .{});
            return;
        }

        std.debug.print("working...\n", .{});
        std.time.sleep(500 * std.time.ns_per_ms);
    }
}

This prints working... a few times, then prints timeout.

Logging with Timestamps

Many programs include timestamps in logs.

A simple debug log might print a raw timestamp:

const std = @import("std");

pub fn main() void {
    const ts = std.time.nanoTimestamp();

    std.debug.print("[{}] program started\n", .{ts});
}

This is not human-friendly, but it is useful for ordering events.

Human-readable date and time formatting is a separate topic. It involves calendars, time zones, leap years, and formatting rules. Keep it separate from basic elapsed-time measurement.

Common Mistakes

Do not assume sleep is exact.

Do not use wall-clock time for precise elapsed-time measurement when monotonic time is available.

Do not trust one timing result.

Do not benchmark debug builds and assume the result represents optimized performance.

Do not let the compiler remove the work you are trying to measure.

Do not confuse nanoseconds, microseconds, milliseconds, and seconds.

The Core Pattern

For elapsed time:

const start = std.time.nanoTimestamp();

// work

const end = std.time.nanoTimestamp();
const elapsed = end - start;

For sleeping:

std.time.sleep(500 * std.time.ns_per_ms);

For readable conversion:

const ms = @as(f64, @floatFromInt(ns)) /
    @as(f64, @floatFromInt(std.time.ns_per_ms));

For repeated timing:

var result: u64 = 0;

const start = std.time.nanoTimestamp();

for (0..100) |_| {
    result ^= work();
}

const end = std.time.nanoTimestamp();

What You Should Remember

std.time contains time-related utilities.

Use timestamps to measure elapsed time.

Subtract start time from end time.

Use constants like std.time.ns_per_ms and std.time.ns_per_s.

Use std.time.sleep for simple delays.

Sleep is approximate.

Timing small code is noisy.

Use release builds for performance measurements.

For serious benchmarking, measure many iterations and make sure the work cannot be optimized away.