clockworklabs/SpacetimeDB · error · InvalidOperationException

uuid counter must be non-negative

Error message

uuid counter must be non-negative

What it means

Thrown by Uuid.FromCounterV7(ref counter, now, randomBytes) when the caller-supplied counter is negative. UUIDv7 encodes a 31-bit monotonic counter into the ID, so the counter must be in [0, 2^31-1]; the method itself increments it and wraps with `& 0x7FFF_FFFF`, so a negative value can only originate from how the caller initialized or mutated the shared variable.

Source

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

    /// Thrown if <paramref name="randomBytes"/> is not exactly 4 bytes long, or <paramref name="now"/> is  before unix epoch.
    /// </exception>
    /// <returns>
    /// A <see cref="Uuid"/> `v7`.
    /// </returns>
    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);

View on GitHub (pinned to 524b4487d9)

Solutions

  1. Initialize the counter to 0 (or any non-negative value) before the first FromCounterV7 call and never decrement it yourself
  2. Audit every write to the shared counter variable - only FromCounterV7 should advance it
  3. When resuming from persisted state, sanitize with counter &= 0x7FFF_FFFF (or reject the state) before generating again
  4. Wrap the call in try/catch (InvalidOperationException) to detect corrupt counter state early and reinitialize

Example fix

// before
int counter = -1; // "uninitialized" sentinel
var id = Uuid.FromCounterV7(ref counter, now, rand4); // throws

// after
int counter = 0;
var id = Uuid.FromCounterV7(ref counter, now, rand4);
Defensive patterns

Strategy: validation

Validate before calling

if (counter < 0 || counter > 0x7FFF_FFFF)
{
    throw new ArgumentOutOfRangeException(nameof(counter), $"Counter out of 31-bit range: {counter}");
}
var id = Uuid.FromCounterV7(ref counter, now, randomBytes);

Try / catch

try
{
    id = Uuid.FromCounterV7(ref counter, now, randomBytes);
}
catch (InvalidOperationException) when (counter < 0)
{
    counter = 0; // reinitialize corrupted state and retry once
    id = Uuid.FromCounterV7(ref counter, now, randomBytes);
}

Prevention

When it happens

Trigger: Calling Uuid.FromCounterV7 with a counter initialized to -1 as an "uninitialized" sentinel; sharing the ref counter with code that decrements it or lets an int overflow into negative territory before the call; restoring a corrupted counter from persisted state.

Common situations: Porting sample code that used -1 sentinels; concurrent generation where another thread mutates the counter; counter state round-tripped through storage/config and re-read with a sign or parsing bug.

Related errors


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