{"record":{"id":"96671047128ece56","repo":"actualbudget/actual","slug":"clock-drift","errorCode":"clock-drift","errorMessage":"clock-drift","messagePattern":"clock-drift","errorType":"error_code","errorClass":"SyncError","httpStatus":null,"severity":"critical","filePath":"packages/loot-core/src/server/sync/index.ts","lineNumber":454,"sourceCode":"  const tables = getTablesFromMessages(messages.filter(msg => !msg.old));\n  app.events.emit('sync', {\n    type: 'applied',\n    tables,\n    data: newData,\n    prevData: oldData,\n  });\n\n  return messages;\n});\n\nexport function receiveMessages(messages: Message[]): Promise<Message[]> {\n  try {\n    messages.forEach(msg => {\n      Timestamp.recv(msg.timestamp);\n    });\n  } catch (e) {\n    if (e instanceof Timestamp.ClockDriftError) {\n      throw new SyncError('clock-drift');\n    }\n    throw e;\n  }\n\n  return runMutator(() => applyMessages(messages));\n}\n\nasync function errorHandler(e: Error) {\n  captureException(e);\n\n  if (e instanceof SyncError) {\n    if (e.reason === 'invalid-schema') {\n      // We know this message came from a local modification, and it\n      // couldn't apply, which doesn't make any sense. Must be a bug\n      // in the code. Send a specific error type for it for a custom\n      // message.\n      app.events.emit('sync', {\n        type: 'error',","sourceCodeStart":436,"sourceCodeEnd":472,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/sync/index.ts#L436-L472","documentation":"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'.","triggerScenarios":"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').","commonSituations":"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.","solutions":["Fix the system clock: enable automatic NTP time sync and let it correct the time","After correcting the clock, restart the app and retry the sync","Check that the time zone and hardware clock (VM/WSL users) are configured consistently","If drift persists, compare the device time against time.is / NTP and reboot after syncing once corrected"],"exampleFix":"// before: clock behind\nawait fullSync(); // clock-drift\n// after: resync system time\nsudo timedatectl set-ntp true && sudo timedatectl status\n// then restart the app and sync","handlingStrategy":"try-catch","validationCode":"const skewMs = Date.now() - localHlcNowMs();\nif (skewMs < -60_000) {\n  throw new Error('System clock appears behind; fix time before syncing');\n}","typeGuard":"function isClockDrift(e: unknown): e is SyncError {\n  return e instanceof SyncError && e.reason?.code === 'clock-drift';\n}","tryCatchPattern":"try {\n  await fullSync();\n} catch (e) {\n  if (isClockDrift(e)) {\n    await resyncSystemClock(); // enable NTP, correct time\n    await fullSync();\n  } else throw e;\n}","preventionTips":["Enable automatic NTP time sync on all devices","Avoid VM snapshot restores of running clients","Verify time before syncing after long offline periods","Monitor clock skew in WSL/containers"],"tags":["sync","clock","time","hlc"],"backgroundTag":"clock-drift","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}