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
- Reduce the rate of Timestamp.send() calls (batch/yield with await so wall time advances)
- Check and fix the local clock drift so lNew tracks Date.now() and the counter resets each new millisecond
- Reinitialize the clock (Timestamp.init clock) after fixing time, ensuring logical time is not stuck ahead
- 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
- Never call Timestamp.send() in an unbounded tight loop; yield to the event loop periodically
- Keep the system clock accurate via NTP so the counter resets on each new millisecond
- Initialize the clock once and monitor for persistent drift
- Cap batch sizes when generating large numbers of entities
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
- TrieNode for key ${k} could not be found
- Timestamp.InvalidError: ${data.timestamp}
- Timestamp.ClockDriftError
- clock-drift
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/5e5de6fdae57ba10.
Report an issue: GitHub.