actualbudget/actual · error · Timestamp.OverflowError

Timestamp.OverflowError

Error message

Timestamp.OverflowError

What it means

Timestamp.OverflowError is thrown by the hybrid logical clock when the per-millisecond counter would exceed MAX_COUNTER (65535) in Timestamp.send(). It means too many timestamps were generated within the same logical millisecond, so the clock cannot guarantee monotonic unique IDs. It signals internal counter exhaustion, not a user-facing condition.

Source

Thrown at packages/crdt/src/crdt/timestamp.ts:218

    // retrieve the local wall time
    const phys = Date.now();

    // unpack the clock.timestamp logical time and counter
    const lOld = clock.timestamp.millis();
    const cOld = clock.timestamp.counter();

    // calculate the next logical time and counter
    // * ensure that the logical time never goes backward
    // * increment the counter if phys time does not advance
    const lNew = Math.max(lOld, phys);
    const cNew = lOld === lNew ? cOld + 1 : 0;

    // check the result for drift and counter overflow
    if (lNew - phys > config.maxDrift) {
      throw new Timestamp.ClockDriftError(lNew, phys, config.maxDrift);
    }
    if (cNew > MAX_COUNTER) {
      throw new Timestamp.OverflowError();
    }

    // repack the logical time/counter
    clock.timestamp.setMillis(lNew);
    clock.timestamp.setCounter(cNew);

    return new Timestamp(
      clock.timestamp.millis(),
      clock.timestamp.counter(),
      clock.timestamp.node(),
    );
  }

  // Timestamp receive. Parses and merges a timestamp from a remote
  // system with the local timeglobal uniqueness and monotonicity are
  // preserved
  static recv(msg: Timestamp): Timestamp | null {
    if (!clock) {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Reduce the rate of Timestamp.send() calls (batch/yield with await so wall time advances)
  2. Check and fix the local clock drift so lNew tracks Date.now() and the counter resets each new millisecond
  3. Reinitialize the clock (Timestamp.init clock) after fixing time, ensuring logical time is not stuck ahead
  4. Catch the error and back off/retry after letting the wall clock advance past lOld

Example fix

// before
timestamps = items.map(() => Timestamp.send());
// after
const timestamps = [];
for (const item of items) {
  let ts = Timestamp.send();
  if (!ts) { await new Promise(r => setTimeout(r, 2)); ts = Timestamp.send(); }
  if (!ts) throw new Error('Timestamp clock exhausted');
  timestamps.push(ts);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Rate-limit sends and ensure the clock is initialized
if (!Timestamp.send()) throw new Error('Clock not initialized');
// Optionally pre-check headroom:
// counter resets each new millisecond, so ensure calls per ms < 65535

Try / catch

try {
  const ts = Timestamp.send();
} catch (e) {
  if (e instanceof Timestamp.OverflowError) {
    await new Promise(r => setTimeout(r, 2)); // let wall time advance
    const ts = Timestamp.send();
  } else throw e;
}

Prevention

When it happens

Trigger: Calling Timestamp.send() more than 65535 times while the local logical time (lOld, possibly advanced ahead of wall time by drift) stays equal to Date.now(), so cNew = cOld + 1 exceeds MAX_COUNTER.

Common situations: Bulk-importing or syncing tens of thousands of entities in a tight loop on a fast machine whose clock has drifted forward (lOld pinned to max(lOld, phys)); long-running client generating many mutations in one millisecond window.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/5e5de6fdae57ba10. Report an issue: GitHub.