Loft's standard library has two time functions, and they answer two different questions:
now() — milliseconds since the Unix epoch (wall-clock time).
ticks() — microseconds elapsed since program start (monotonic clock).
Both return an integer (loft's 64-bit integer) — which they need, since a millisecond timestamp passed 32 bits in 1970 plus a few weeks.
Use now() when you want to know WHEN something happened: timestamps, log entries, anything you will compare against a calendar. Use ticks() when you want to know HOW LONG something took: benchmarks and frame timing. It is unaffected by system clock changes or NTP adjustments, which is exactly what now() cannot promise.
Calendars are a library
These two are the whole clock surface: there is no year, month or weekday in the standard library, and no formatting. That is deliberate — calendar arithmetic is a large, opinionated subject — and it is a package away:
use time::*;
fn main() {
ms = now();
println(format_iso(ms)); // 2026-09-01T09:03:58Z
println("{year(ms)} {month_name(ms)} {day(ms)}, a {weekday_name(ms)}");
println(format_date(add_days(ms, 30))); // 2026-10-01
}
The time package works on the same millisecond-since-epoch integer now() returns, so nothing has to be converted. Install it with loft install time.
fn main() {
Wall-clock time: now()
now() returns the current time as milliseconds since 1970-01-01T00:00:00 UTC, so it is well past the range a 32-bit number could hold.
t = now();
assert(t > 2147483647, "now() is a 64-bit millisecond stamp: {t}");
It reads the system clock, so it usually moves forward — but it is the system's clock, not yours: an NTP correction or a manual change can step it in either direction. Never subtract two now() values to time something. That is what ticks() is for.
t2 = now();
assert(t2 > 2147483647, "a second reading is a timestamp too: {t2}");
Elapsed time: ticks()
ticks() measures microseconds since the program started, so it begins near zero and only ever climbs. It uses a monotonic clock, which is the guarantee now() does not have: it never jumps backward, whatever happens to the system clock while your program runs.
start = ticks();
assert(start >= 0, "ticks() must be non-negative");
end = ticks();
assert(end >= start, "ticks() must be monotonically non-decreasing");
Measuring elapsed time
Subtract two ticks() values to get the duration in microseconds. Divide by 1000 to convert to milliseconds.
elapsed_us = end_ticks - start_ticks
elapsed_ms = elapsed_us / 1000
Example: measure how long a loop takes.
t_before = ticks();
sum = 0;
for i in 0..1000 {
sum += i
}
t_after = ticks();
elapsed = t_after - t_before;
assert(elapsed >= 0, "elapsed time must be non-negative: {elapsed}");
assert(sum == 499500, "loop produced wrong sum: {sum}");
Common patterns
Timestamp a log entry (seconds since epoch): seconds = now() / 1000
Seed the random number generator with the current time. The generator is in the random package rather than the standard library, so the import is part of the pattern — without it the name does not resolve: use random::(rand_seed); rand_seed(now())
Simple stopwatch (integer division, so this truncates to whole ms): start = ticks() ... do work ... log_info("Done in {(ticks() - start) / 1000} ms")
}
Run it yourself
This page is a loft program. It runs here, in your browser — press Run, then ask it something.
Stop on a line
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// @NAME: Time
// @TITLE: Wall-clock time and elapsed time
// Loft's standard library has two time functions, and they answer two
// different questions:
//
// now() — milliseconds since the Unix epoch (wall-clock time).
// ticks() — microseconds elapsed since program start (monotonic clock).
//
// Both return an 'integer' (loft's 64-bit integer) — which they need, since a
// millisecond timestamp passed 32 bits in 1970 plus a few weeks.
//
// Use 'now()' when you want to know WHEN something happened: timestamps, log
// entries, anything you will compare against a calendar.
// Use 'ticks()' when you want to know HOW LONG something took: benchmarks and
// frame timing. It is unaffected by system clock changes or NTP adjustments,
// which is exactly what 'now()' cannot promise.
//
// ## Calendars are a library
// These two are the whole clock surface: there is no year, month or weekday
// in the standard library, and no formatting. That is deliberate — calendar
// arithmetic is a large, opinionated subject — and it is a package away:
//
// use time::*;
// fn main() {
// ms = now();
// println(format_iso(ms)); // 2026-09-01T09:03:58Z
// println("{year(ms)} {month_name(ms)} {day(ms)}, a {weekday_name(ms)}");
// println(format_date(add_days(ms, 30))); // 2026-10-01
// }
//
// The 'time' package works on the same millisecond-since-epoch integer 'now()'
// returns, so nothing has to be converted. Install it with 'loft install time'.
fn main() {
// ## Wall-clock time: now()
// 'now()' returns the current time as milliseconds since 1970-01-01T00:00:00 UTC,
// so it is well past the range a 32-bit number could hold.
t = now();
assert(t > 2147483647, "now() is a 64-bit millisecond stamp: {t}");
// It reads the system clock, so it usually moves forward — but it is the
// system's clock, not yours: an NTP correction or a manual change can step it
// in either direction. Never subtract two 'now()' values to time something.
// That is what 'ticks()' is for.
t2 = now();
assert(t2 > 2147483647, "a second reading is a timestamp too: {t2}");
// ## Elapsed time: ticks()
// 'ticks()' measures microseconds since the program started, so it begins
// near zero and only ever climbs. It uses a monotonic clock, which is the
// guarantee 'now()' does not have: it never jumps backward, whatever happens
// to the system clock while your program runs.
start = ticks();
assert(start >= 0, "ticks() must be non-negative");
end = ticks();
assert(end >= start, "ticks() must be monotonically non-decreasing");
// ## Measuring elapsed time
// Subtract two 'ticks()' values to get the duration in microseconds.
// Divide by 1000 to convert to milliseconds.
//
// elapsed_us = end_ticks - start_ticks
// elapsed_ms = elapsed_us / 1000
//
// Example: measure how long a loop takes.
t_before = ticks();
sum = 0;
for i in 0..1000 {
sum += i
}
t_after = ticks();
elapsed = t_after - t_before;
assert(elapsed >= 0, "elapsed time must be non-negative: {elapsed}");
assert(sum == 499500, "loop produced wrong sum: {sum}");
// ## Common patterns
//
// Timestamp a log entry (seconds since epoch):
// seconds = now() / 1000
//
// Seed the random number generator with the current time. The generator is
// in the 'random' package rather than the standard library, so the import is
// part of the pattern — without it the name does not resolve:
// use random::(rand_seed);
// rand_seed(now())
//
// Simple stopwatch (integer division, so this truncates to whole ms):
// start = ticks()
// ... do work ...
// log_info("Done in {(ticks() - start) / 1000} ms")
}