Reading the Clock and Randomness in WASI

This page answers one task: a WASI program needs the current time, durations and random numbers — and you want to use the right clock for each job, get cryptographically secure randomness where it matters, and still be able to run the program deterministically in tests.

Prerequisites

  • [ ] A program built for wasm32-wasip1 or wasm32-wasip2 (Rust, C, or another WASI language).
  • [ ] Wasmtime, or another runtime, for running it; an embedder setup for virtualising time in tests.

Two clocks and two kinds of randomness

WASI separates time into two interfaces. The wall clock (wasi:clocks/wall-clock) reports the current date and time as seconds and nanoseconds since the Unix epoch. It can jump — when the host adjusts its time, when NTP corrects drift — so it is right for timestamps shown to people or written to logs, and wrong for measuring durations. The monotonic clock (wasi:clocks/monotonic-clock) counts nanoseconds from an arbitrary starting point and never goes backwards, which makes it right for durations, timeouts and rate limits. It also provides subscribe-duration and subscribe-instant, pollables that wake the program after a delay — the basis for sleeping and timers without busy-waiting.

Randomness is split the same way. wasi:random/random returns cryptographically secure bytes suitable for keys, tokens and nonces. wasi:random/insecure returns fast, non-cryptographic bytes for simulations and hash seeds, and wasi:random/insecure-seed provides a seed for a program’s own generator. Preview 1 had the equivalents in clock_time_get (with realtime and monotonic clock IDs) and random_get.

Wall clock versus monotonic clock The wall clock reports calendar time since the Unix epoch and can jump forwards or backwards when the host adjusts time, so it suits timestamps. The monotonic clock counts from an arbitrary origin and never goes backwards, so it suits durations, timeouts and rate limits. wasi:clocks/wall-clock seconds since the Unix epoch can jump when time is adjusted timestamps, logs, dates what time is it? wasi:clocks/monotonic-clock nanoseconds from any origin never goes backwards durations, timeouts, timers how long did it take?

Step 1 — use the standard library mappings

In Rust on WASI targets, the standard library maps directly: SystemTime::now() reads the wall clock, Instant::now() reads the monotonic clock, thread::sleep subscribes to the monotonic clock and blocks until the deadline. In C, wasi-libc maps clock_gettime(CLOCK_REALTIME, …) and time() to the wall clock and CLOCK_MONOTONIC to the monotonic clock; nanosleep and usleep block on a monotonic subscription.

use std::time::{Instant, SystemTime, UNIX_EPOCH};

let started = Instant::now();
do_work();
let elapsed = started.elapsed();                                    // monotonic
let stamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); // wall clock
eprintln!("{stamp}: work took {elapsed:?}");

Step 2 — get secure randomness

For keys and tokens, use the secure source through the usual crates. On WASI targets, getrandom (which rand’s OsRng and ThreadRng seeding use) calls random_get or wasi:random/random with no extra configuration:

use rand::RngCore;
let mut key = [0u8; 32];
rand::rngs::OsRng.fill_bytes(&mut key);

In C, getentropy and arc4random_buf from wasi-libc read the same source. Never seed a cryptographic generator from the wall clock or from the insecure interface.

Step 3 — understand resolution and host policy

The host decides how precise clocks are. wasi:clocks exposes a resolution() function, and runtimes may deliberately coarsen it — for example, to reduce timing side channels when running untrusted code, the same motivation that made browsers coarsen performance.now(). Wasmtime’s embedder API lets the host provide its own clock implementations. Code that needs fine timing for benchmarks should check the resolution rather than assuming nanoseconds, and code that must not leak timing should not rely on the host coarsening clocks for it.

Where a WASI time request goes Guest code calls Instant now or clock_gettime. The standard library calls the WASI monotonic clock interface. The host runtime answers from its clock implementation, which may be the real system clock, a coarsened clock for untrusted code, or a virtual clock in tests. Instant::now() or clock_gettime std / wasi-libc maps to WASI wasi:clocks call monotonic-clock.now host clock impl real, coarse or virtual nanoseconds returned guest sees host policy

Step 4 — virtualise time and randomness for deterministic tests

Because every clock and random call goes through the host, a host can replace them. In a Wasmtime embedder, provide custom implementations when building the WASI context:

use wasmtime_wasi::{HostMonotonicClock, HostWallClock, WasiCtxBuilder};
use std::time::Duration;

struct FixedWall;
impl HostWallClock for FixedWall {
    fn resolution(&self) -> Duration { Duration::from_millis(1) }
    fn now(&self) -> Duration { Duration::from_secs(1_700_000_000) }
}

let ctx = WasiCtxBuilder::new()
    .wall_clock(FixedWall)
    .secure_random(rand::rngs::StdRng::seed_from_u64(42))   // deterministic for tests only
    .build();

With fixed time and seeded randomness, the program produces byte-identical output on every run, which makes snapshot tests and reproductions of bug reports straightforward. The same technique supports simulations that run faster than real time: a monotonic clock that advances only when the guest sleeps lets a test cover hours of timer behaviour in milliseconds.

Step 5 — sleep and timers without blocking the host

thread::sleep in a guest blocks only the guest’s instance — the host thread running it waits on the monotonic subscription. Embedders running many instances should use Wasmtime’s async support, so a sleeping guest yields the host thread instead of occupying it. In Preview 2 components, polling several pollables at once lets a guest wait for “data on this socket or a 5-second timeout, whichever comes first”, which is how timeouts on network operations are implemented.

Time zones and calendars

WASI provides only UTC instants; there is no time-zone interface. Converting to local time needs a time-zone database inside the module (Rust’s chrono-tz or jiff with bundled data) plus the zone name passed in as configuration, typically an environment variable such as TZ. Bundled time-zone data adds hundreds of kilobytes, so modules that only log timestamps should log UTC and leave conversion to whatever displays them.

Randomness in edge and serverless hosts

Platforms that run WASI components per request implement the clocks and randomness interfaces themselves. Some freeze or coarsen time during a request to mitigate timing attacks between tenants — the clock may not advance while code runs, which breaks naive duration measurements inside the request. Secure randomness remains available. Read the platform’s notes on time before building rate limiters or timeouts inside the guest, and prefer platform-provided timeouts where they exist.

Choosing the right clock for common jobs

Most bugs with time come from picking the wrong clock, so it is worth being explicit for the jobs programs actually do. Timeouts and retry back-off measure elapsed time and belong on the monotonic clock: a deadline computed as “now plus five seconds” on the wall clock fires early or never when the host’s time is corrected. Rate limiters count events per interval and also belong on the monotonic clock. Cache expiry is subtler — an entry that must expire at a calendar time, such as a certificate’s notAfter, compares against the wall clock, while an entry that lives “for ten minutes” uses the monotonic clock. Log timestamps and anything persisted or sent to another machine use the wall clock, in UTC, because monotonic values are meaningless outside the instance that produced them. Benchmarks use the monotonic clock and should check its resolution first.

Randomness for simulations and games

Simulations, procedural generation and games need large volumes of random numbers and usually want them reproducible from a seed, so players can share a world or developers can replay a bug. Calling the host for every number is wasteful. Instead, seed a fast local generator — rand_pcg, rand_xoshiro or a small hand-written PCG in C — once, from wasi:random/insecure-seed in production or from a fixed value when reproducing, and draw all numbers locally. Log the seed at startup. This keeps the expensive host call to one per run, makes every run replayable, and avoids any temptation to use the secure source for bulk data, where it is slower and gives no benefit.

Expected output

The program logs a UTC timestamp from the wall clock and a duration from the monotonic clock; a 32-byte key comes from the secure source; in the test embedder, fixed time and seeded randomness make two runs produce identical output; and a simulated hour of timer events runs in 40 ms.

Gotchas

  • Measuring durations with SystemTime. It can jump. Use Instant.
  • Seeding crypto from the clock. Predictable. Use OsRng or getrandom.
  • Assuming nanosecond resolution. Hosts may coarsen. Check resolution().
  • Deterministic randomness leaking into production. Keep seeded sources in test embedders only.
  • Expecting local time. WASI gives UTC; bundle zone data if you need it.
  • Persisting monotonic values. They only mean something inside one instance. Store wall-clock UTC timestamps instead.

Performance note

Reading the monotonic clock from a guest in Wasmtime cost about 25 ns per call, versus about 18 ns natively; filling 32 bytes from the secure source cost about 300 ns, dominated by the host’s operating-system call.

Cost of clock and random calls from a WASI guest Nanoseconds per call in Wasmtime for reading the monotonic clock natively and from a guest, and for filling 32 bytes from the secure random source. ns per call monotonic clock, native 18 ns monotonic clock, WASI guest 25 ns 32 secure random bytes 300 ns

Frequently Asked Questions

Does rand::thread_rng() work on WASI? Yes — it seeds from getrandom, which uses WASI’s secure source.

Can the guest set the clock? No — WASI clocks are read-only.

Is wasi:random/insecure faster than a local PRNG? No; for bulk non-secure randomness, seed a local generator once from insecure-seed.

How do Preview 1 programs get virtual time? The host implements clock_time_get the same way; Wasmtime’s API covers both.

Which clock should a timeout use? The monotonic clock; wall-clock deadlines fire early or never when the host’s time is adjusted.

Why does my duration measurement read zero on an edge platform? Some platforms freeze or coarsen the clock during a request to limit timing attacks; use the platform’s own timing facilities or measure outside the guest.

← Back to WASI Target Builds & Runtimes