clockworklabs/SpacetimeDB · error

timestamp before unix epoch

Error message

timestamp before unix epoch

What it means

`Uuid::from_counter_v7` builds a UUIDv7 whose leading 48 bits are milliseconds since the Unix epoch, so the timestamp must be at/after 1970; the code calls `now.to_duration_since_unix_epoch().expect("timestamp before unix epoch")`. A pre-epoch Timestamp therefore panics — in practice meaning the host clock (or a test-supplied Timestamp) reads before the epoch.

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 524b4487d9)

Solutions

  1. Sync the host clock (NTP/systemd-timesyncd) and verify `date` is sane.
  2. In tests, use Timestamp::UNIX_EPOCH or later values.
  3. If pre-epoch inputs are possible in your flow, gate the call on `now >= Timestamp::UNIX_EPOCH` and fail with a proper error or use a random UUID.

Example fix

// before: panics when now < epoch
let id = Uuid::from_counter_v7(&counter, now, &random_bytes)?;

// after: guard the clock before generating
if now.to_duration_since_unix_epoch().is_err() {
    return Err(anyhow::anyhow!("clock pre-dates Unix epoch; refusing to mint UUIDv7"));
}
let id = Uuid::from_counter_v7(&counter, now, &random_bytes)?;
Defensive patterns

Strategy: validation

Validate before calling

if now.to_duration_since_unix_epoch().is_err() {
    return Err("cannot build UUIDv7 with pre-epoch timestamp".into());
}
let id = Uuid::from_counter_v7(&counter, now, &random_bytes)?;

Prevention

When it happens

Trigger: Generating ids via from_counter_v7 while the system clock is set before 1970 (dead RTC, VM clone artifact, severe clock skew), or passing `Timestamp::from_micros_since_unix_epoch(negative)` in tests.

Common situations: Unsynchronized VMs/containers with broken clocks; embedded hosts with dead RTC batteries; tests that deliberately exercise pre-epoch timestamps.

Related errors


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