overleaf/overleaf · error · BaseVersionConflictError
base version mismatch
Error message
base version mismatch
What it means
queueChanges pushes changes onto the Redis pending-changes list guarded by the project's head version. When the Lua script reports status 'conflict', the caller's baseVersion does not match the current head version, and a BaseVersionConflictError is thrown with both values attached. This is the store's optimistic-concurrency mechanism protecting the change sequence.
Source
Thrown at services/history-v1/storage/lib/chunk_store/redis.js:210
const args = [
baseVersion.toString(),
JSON.stringify(headSnapshot.toRaw()),
persistTime.toString(),
expireTime.toString(),
onlyIfExists.toString(), // Only queue changes if the snapshot already exists
...changes.map(change => JSON.stringify(change.toRaw())), // Serialize changes
]
const status = await rclient.queue_changes(keys, args)
metrics.inc('chunk_store.redis.queue_changes', 1, { status })
if (status === 'ok') {
return status
}
if (status === 'ignore') {
return status // skip changes when project does not exist and onlyIfExists is true
}
if (status === 'conflict') {
throw new BaseVersionConflictError('base version mismatch', {
projectId,
baseVersion,
})
} else {
throw new OError('unexpected result queuing changes', { status })
}
} catch (err) {
if (err instanceof BaseVersionConflictError) {
// Re-throw conflict errors directly
throw err
}
metrics.inc('chunk_store.redis.queue_changes', 1, { status: 'error' })
throw err
}
}
rclient.defineCommand('get_state', {
numberOfKeys: 6, // Number of keys defined in keySchemaView on GitHub (pinned to 28ad3b03b7)
Solutions
- Catch BaseVersionConflictError, reload the latest head snapshot, rebase the changes onto it, and retry queueChanges with the new baseVersion.
- Before queuing, fetch the current head version and refuse/refresh when it differs from your baseVersion.
- Ensure each client session refreshes its base version after any conflict or reconnect instead of retrying blindly.
Example fix
// before
await queueChanges(projectId, snapshot, baseVersion, changes)
// after
try {
await queueChanges(projectId, snapshot, baseVersion, changes)
} catch (err) {
if (err instanceof BaseVersionConflictError) {
const fresh = await chunkStore.loadHead(projectId)
const rebased = rebaseChanges(changes, snapshot, fresh.getSnapshot())
await queueChanges(projectId, fresh.getSnapshot(), fresh.getVersion(), rebased)
} else {
throw err
}
} Defensive patterns
Strategy: retry
Validate before calling
const head = await chunkStore.getProjectHeadVersion(projectId); if (head !== baseVersion) { await refreshBaseSnapshot(); } Type guard
function isBaseVersionConflict(err) { return err instanceof BaseVersionConflictError } Try / catch
try { await queueChanges(projectId, snapshot, baseVersion, changes) } catch (err) { if (isBaseVersionConflict(err)) { await rebaseAndRetry(projectId, changes, err.info) } else { throw err } } Prevention
- Re-read the head version right before queuing after any idle period or reconnect.
- Rebase changes onto the current snapshot instead of retrying with a stale base.
- Cap retry attempts and surface a merge conflict to the user after repeated conflicts.
When it happens
Trigger: Calling queueChanges with a baseVersion computed from a stale head snapshot while another client has already appended changes; two editors publishing concurrently from the same base version.
Common situations: Long-lived editor sessions whose head version fell behind after reconnects; a second tab/device editing the same project; a retry of an old queued batch after the version advanced.
Related errors
- document has been updated, aborting overwrite. Try again.
- unable to close chunk: not found
- unable to close chunk: already closed
- Non-persisted changes can't be applied to base version
- Persisted version cannot be higher than head version
AI-assisted analysis of overleaf/overleaf@28ad3b03b7 (2026-09-03).
Data as JSON: /api/errors/4f26a560d73a38bb.
Report an issue: GitHub.