actualbudget/actual · critical · SyncError

clock-drift

clock-drift

Error message

clock-drift

What it means

A SyncError thrown by receiveMessages() when Timestamp.recv() detects a clock drift: a message arrives whose timestamp appears to come from the local node's future, meaning this device's clock is behind or was changed backwards. Because hybrid logical clocks can't safely accept such messages, sync aborts with 'clock-drift'.

Source

Thrown at packages/loot-core/src/server/sync/index.ts:454

  const tables = getTablesFromMessages(messages.filter(msg => !msg.old));
  app.events.emit('sync', {
    type: 'applied',
    tables,
    data: newData,
    prevData: oldData,
  });

  return messages;
});

export function receiveMessages(messages: Message[]): Promise<Message[]> {
  try {
    messages.forEach(msg => {
      Timestamp.recv(msg.timestamp);
    });
  } catch (e) {
    if (e instanceof Timestamp.ClockDriftError) {
      throw new SyncError('clock-drift');
    }
    throw e;
  }

  return runMutator(() => applyMessages(messages));
}

async function errorHandler(e: Error) {
  captureException(e);

  if (e instanceof SyncError) {
    if (e.reason === 'invalid-schema') {
      // We know this message came from a local modification, and it
      // couldn't apply, which doesn't make any sense. Must be a bug
      // in the code. Send a specific error type for it for a custom
      // message.
      app.events.emit('sync', {
        type: 'error',

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Fix the system clock: enable automatic NTP time sync and let it correct the time
  2. After correcting the clock, restart the app and retry the sync
  3. Check that the time zone and hardware clock (VM/WSL users) are configured consistently
  4. If drift persists, compare the device time against time.is / NTP and reboot after syncing once corrected

Example fix

// before: clock behind
await fullSync(); // clock-drift
// after: resync system time
sudo timedatectl set-ntp true && sudo timedatectl status
// then restart the app and sync
Defensive patterns

Strategy: try-catch

Validate before calling

const skewMs = Date.now() - localHlcNowMs();
if (skewMs < -60_000) {
  throw new Error('System clock appears behind; fix time before syncing');
}

Type guard

function isClockDrift(e: unknown): e is SyncError {
  return e instanceof SyncError && e.reason?.code === 'clock-drift';
}

Try / catch

try {
  await fullSync();
} catch (e) {
  if (isClockDrift(e)) {
    await resyncSystemClock(); // enable NTP, correct time
    await fullSync();
  } else throw e;
}

Prevention

When it happens

Trigger: receiveMessages() iterates messages from the server and calls Timestamp.recv(msg.timestamp); if any timestamp exceeds the local clock's allowed bound, Timestamp.ClockDriftError is thrown and converted to SyncError('clock-drift').

Common situations: System clock was set backwards (manual change, dead CMOS battery, VM snapshot restore, timezone/NTP misconfiguration); syncing a budget that received messages from another device while this device was paused/restored from a snapshot.

Related errors


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