actualbudget/actual · critical · SyncError
out-of-sync
out-of-sync
Error message
out-of-sync
What it means
A SyncError thrown by _fullSync() after too many sync round-trips (10 rounds with no change in diffTime, or 100 rounds total) still cannot reconcile the local Merkle tree with the server's. The client logs both trees and gives up with 'out-of-sync', meaning local and server message histories have diverged in a way normal incremental syncing cannot repair.
Source
Thrown at packages/loot-core/src/server/sync/index.ts:816
'server hash:',
res.merkle.hash,
'localTimeChanged:',
localTimeChanged,
);
if (rebuiltMerkle.trie.hash === res.merkle.hash) {
// Rebuilding the merkle worked... but why?
const clocks = await db.all<db.DbClockMessage>(
'SELECT * FROM messages_clock',
);
if (clocks.length !== 1) {
logger.log('Bad number of clocks:', clocks.length);
}
const hash = deserializeClock(clocks[0].clock).merkle.hash;
logger.log('Merkle hash in db:', hash);
}
throw new SyncError('out-of-sync');
}
receivedMessages = receivedMessages.concat(
await _fullSync(
new Timestamp(diffTime, 0, '0').toString(),
// If something local changed while we were syncing, always
// reset, token the counter. We never want to think syncing failed
// because we tried to syncing many times and couldn't sync,
// but it was because the user kept changing stuff in the
// middle of syncing.
localTimeChanged ? 0 : count + 1,
diffTime,
),
);
} else {
// All synced up, store the current time as a simple optimization for the next sync
const requiresUpdate =
getClock().timestamp.toString() !== lastSyncedTimestamp;View on GitHub (pinned to d4334cb6e6)
Solutions
- Back up the budget, then use the 'repair' / reset sync data option so the server group and merkle tree are rebuilt from the local data
- Verify the sync server was not restored from an older backup; restore matching backups if it was
- Clear the local budget db and re-download the budget fresh from the server
- Check sync-server logs/messages table for manual tampering or pruning and stop doing manual db edits
Example fix
// before: persistent out-of-sync await fullSync(); // out-of-sync // after: reset sync data on the authoritative client // File → 'Reset sync data' / repair endpoint, then let other devices re-download
Defensive patterns
Strategy: retry
Validate before calling
const localHash = getClock().merkle.hash;
const serverHash = await fetchServerMerkleHash(groupId);
if (localHash !== serverHash) {
logger.warn('Merkle hashes differ; may need sync repair');
} Type guard
function isOutOfSync(e: unknown): e is SyncError {
return e instanceof SyncError && e.reason?.code === 'out-of-sync';
} Try / catch
try {
await fullSync();
} catch (e) {
if (isOutOfSync(e)) {
await resetSyncData(); // repair: rebuild server group from local
} else throw e;
} Prevention
- Never restore the sync server from an older backup without also resetting client sync data
- Avoid manual edits to the server messages table
- Take backups before major sync-server maintenance
- Rebuild merkle via the repair tool at first symptom rather than repeated retries
When it happens
Trigger: fullSync loops: local merkle hash != server merkle hash after receiving messages, diffTime stops advancing, count exceeds the limit → SyncError('out-of-sync').
Common situations: Server database was restored from backup or its messages were pruned/manually modified; a client force-synced with corrupted local clock/merkle state; very old budgets where clock history diverged; duplicate budgets pointing at the same group id after a bad restore.
Related errors
- TrieNode for key ${k} could not be found
- Sync ID is required for sync ${flag}. Set --sync-id or ACTUA
- Could not resolve on-disk budget id for syncId ${syncId} aft
- Timestamp.InvalidError: ${data.timestamp}
- Timestamp.ClockDriftError
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/c78c726c038b2c03.
Report an issue: GitHub.