clockworklabs/SpacetimeDB · error · ArgumentException

timestamp before unix epoch

Error message

timestamp before unix epoch

What it means

Thrown by Uuid.FromCounterV7 when the passed Timestamp is before 1970-01-01T00:00:00 UTC. UUIDv7 embeds a 48-bit unix_ts_ms field which cannot represent negative unix time, so the library rejects pre-epoch timestamps via ArgumentException on the `now` parameter.

Source

Thrown at crates/bindings-csharp/BSATN.Runtime/BSATN/Uuid.cs:137

    public static Uuid FromCounterV7(
        ref int counter,
        Timestamp now,
        ReadOnlySpan<byte> randomBytes // must be length 4
    )
    {
        if (randomBytes.Length != 4)
        {
            throw new ArgumentException("randomBytes must be exactly 4 bytes", nameof(randomBytes));
        }

        if (counter < 0)
        {
            throw new InvalidOperationException("uuid counter must be non-negative");
        }

        if (now.MicrosecondsSinceUnixEpoch < 0)
        {
            throw new ArgumentException("timestamp before unix epoch", nameof(now));
        }
        var unixTsMs = now.MicrosecondsSinceUnixEpoch / 1_000;

        // monotonic 31-bit
        var counterVal = counter;
        counter = (counter + 1) & 0x7FFF_FFFF;

        Span<byte> bytes = stackalloc byte[16];

        // unix_ts_ms (48 bits, big-endian)
        var ts = unixTsMs & 0x0000_FFFF_FFFF_FFFFL;
        bytes[0] = (byte)(ts >> 40);
        bytes[1] = (byte)(ts >> 32);
        bytes[2] = (byte)(ts >> 24);
        bytes[3] = (byte)(ts >> 16);
        bytes[4] = (byte)(ts >> 8);
        bytes[5] = (byte)ts;

View on GitHub (pinned to 524b4487d9)

Solutions

  1. Pass the current time (Timestamp.UtcNow / DateTimeOffset.UtcNow conversion) instead of the entity's business date
  2. If you must derive from a date, clamp it: now < epoch ? epoch : now
  3. Fix the system clock / NTP configuration if the machine reports pre-epoch time
  4. Validate ts.MicrosecondsSinceUnixEpoch >= 0 before calling and log the offending source value

Example fix

// before
var ts = new Timestamp(new DateTimeOffset(1969, 7, 20, 0, 0, 0, TimeSpan.Zero));
var id = Uuid.FromCounterV7(ref counter, ts, rand4); // throws

// after
var now = Timestamp.UtcNow; // IDs use generation time, not business dates
var id = Uuid.FromCounterV7(ref counter, now, rand4);
Defensive patterns

Strategy: validation

Validate before calling

static Timestamp ClampToEpoch(Timestamp ts) =>
    ts.MicrosecondsSinceUnixEpoch < 0 ? new Timestamp(0) : ts;

var id = Uuid.FromCounterV7(ref counter, ClampToEpoch(now), randomBytes);

Try / catch

try
{
    id = Uuid.FromCounterV7(ref counter, now, randomBytes);
}
catch (ArgumentException ex) when (ex.ParamName == "now")
{
    logger.LogWarning("Pre-epoch timestamp {Ts} clamped for UUIDv7", now);
    id = Uuid.FromCounterV7(ref counter, new Timestamp(0), randomBytes);
}

Prevention

When it happens

Trigger: Passing a Timestamp converted from a DateTimeOffset/DateTime before 1970 (historical dates, birthdates, zeroed DateTime values interpreted oddly); a machine clock set before the epoch; arithmetic on MicrosecondsSinceUnixEpoch that underflows below zero.

Common situations: Batch-importing historical records and generating IDs from their dates; misconfigured VM/container clocks; test fixtures with fixed pre-epoch dates.

Related errors


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