clockworklabs/SpacetimeDB · error

timestamp before unix epoch

Error message

timestamp before unix epoch

What it means

Panic in `Uuid::from_counter_v7`, SpacetimeDB's UUIDv7 generator. It converts the `now: Timestamp` argument via `to_duration_since_unix_epoch().expect("timestamp before unix epoch")`; a Timestamp earlier than 1970 returns `Err` (the Err encodes the pre-epoch offset) and the expect panics. UUIDv7 embeds a positive millisecond timestamp, so pre-epoch times are unsupported by construction.

Source

Thrown at crates/sats/src/uuid.rs:133

    /// let counter = std::cell::Cell::new(1);
    /// // Use the `ReducerContext::rng()` | `ProcedureContext::rng()` to generate random bytes,
    /// // or call `ReducerContext::new_uuid_v7()` / `ProcedureContext::new_uuid_v7()`
    /// let random_bytes = [0u8; 4];
    /// let uuid = Uuid::from_counter_v7(&counter, now, &random_bytes).unwrap();
    ///
    /// assert_eq!(
    ///     "0000647e-5180-7000-8000-000200000000",
    ///     uuid.to_string(),
    /// );
    /// ```
    pub fn from_counter_v7(counter: &Cell<u32>, now: Timestamp, random_bytes: &[u8; 4]) -> anyhow::Result<Self> {
        // Monotonic counter value (31 bits)
        let counter_val = counter.get();
        counter.set(counter_val.wrapping_add(1) & 0x7FFF_FFFF);

        let ts_ms = now
            .to_duration_since_unix_epoch()
            .expect("timestamp before unix epoch")
            .as_millis() as i64
            & 0xFFFFFFFFFFFF;

        let mut bytes = [0u8; 16];

        // unix_ts_ms (48 bits)
        bytes[0] = (ts_ms >> 40) as u8;
        bytes[1] = (ts_ms >> 32) as u8;
        bytes[2] = (ts_ms >> 24) as u8;
        bytes[3] = (ts_ms >> 16) as u8;
        bytes[4] = (ts_ms >> 8) as u8;
        bytes[5] = ts_ms as u8;

        // version & variant
        // bytes[6] = uuid::Version::SortRand;
        // bytes[8] = Variant::RFC4122

        // Counter bits

View on GitHub (pinned to 6dee26c6ef)

Solutions

  1. Check and fix the system clock (enable NTP; verify `date -u` shows post-1970) — the clock, not the code, is usually wrong.
  2. Guard ID-generation call sites: `if now.to_duration_since_unix_epoch().is_err() { use fallback }` or clamp `now` to `Timestamp::UNIX_EPOCH`.
  3. Validate any externally-supplied Timestamp before passing it to `from_counter_v7`.
  4. In tests, use realistic fixed timestamps (post-1970) rather than epoch-relative negatives.

Example fix

// before: panics when clock/ts predates 1970
let id = Uuid::from_counter_v7(&counter, now, &rand)?;

// after: clamp pre-epoch times to the epoch
let now = if now.to_duration_since_unix_epoch().is_err() { Timestamp::UNIX_EPOCH } else { now };
let id = Uuid::from_counter_v7(&counter, now, &rand)?;
Defensive patterns

Strategy: validation

Validate before calling

fn uuid_now(now: Timestamp, counter: &Cell<u32>, rnd: &[u8; 4]) -> anyhow::Result<Uuid> {
    let now = if now.to_duration_since_unix_epoch().is_err() {
        Timestamp::UNIX_EPOCH // clamp: UUIDv7 cannot encode pre-epoch
    } else { now };
    Ok(Uuid::from_counter_v7(counter, now, rnd)?)
}

Try / catch

let r = std::panic::catch_unwind(AssertUnwindSafe(|| Uuid::from_counter_v7(&c, now, &rnd)));
match r { Ok(v) => v?, Err(_) => return Err(ClockError::pre_epoch) }

Prevention

When it happens

Trigger: Generating a UUIDv7 while the system clock (or the injected `now` value) reads before 1970-01-01: broken RTC/VM clock, NTP step-back in tests, or code passing a deliberately negative Timestamp (e.g. `UNIX_EPOCH - x` arithmetic) into ID generation.

Common situations: Machines booting with dead CMOS batteries into pre-1970 dates; sandboxes/containers with clock skew; test fixtures constructing fixed Timestamps below the epoch; feeding historical dates into ID generation by mistake.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20). Data as JSON: /api/errors/4581b4c3cfd50203. Report an issue: GitHub.