overleaf/overleaf · error · VersionOutOfBoundsError

Non-persisted changes can't be applied to base version

Error message

Non-persisted changes can't be applied to base version

What it means

getNonPersistedChanges asks the Redis script for changes after a given baseVersion. Status 'out_of_bounds' means the requested baseVersion is no longer within the non-persisted window — the changes starting at that version were already persisted into postgres (or compacted away) — so a VersionOutOfBoundsError is thrown.

Source

Thrown at services/history-v1/storage/lib/chunk_store/redis.js:487

    )
  } catch (err) {
    metrics.inc('chunk_store.redis.get_non_persisted_changes', 1, {
      status: 'error',
    })
    throw err
  }

  const status = result[0]
  metrics.inc('chunk_store.redis.get_non_persisted_changes', 1, {
    status,
  })

  if (status === 'ok') {
    return result[1].map(json => Change.fromRaw(JSON.parse(json)))
  } else if (status === 'not_found') {
    return []
  } else if (status === 'out_of_bounds') {
    throw new VersionOutOfBoundsError(
      "Non-persisted changes can't be applied to base version",
      { projectId, baseVersion }
    )
  } else {
    throw new OError('unknown status for get_non_persisted_changes', {
      projectId,
      baseVersion,
      status,
    })
  }
}

rclient.defineCommand('set_persisted_version', {
  numberOfKeys: 4,
  lua: `
    local headVersionKey = KEYS[1]
    local persistedVersionKey = KEYS[2]
    local persistTimeKey = KEYS[3]

View on GitHub (pinned to 28ad3b03b7)

Solutions

  1. Catch VersionOutOfBoundsError and fall back to loading the persisted chunk history from postgres for versions at/below the persisted boundary.
  2. Clamp baseVersion to the store's current non-persisted start version before calling.
  3. Subscribe to/track the persisted version and only call this API for versions > persistedVersion.

Example fix

// before
const changes = await chunkStore.getNonPersistedChanges(projectId, baseVersion)

// after
let changes
try {
  changes = await chunkStore.getNonPersistedChanges(projectId, baseVersion)
} catch (err) {
  if (err instanceof VersionOutOfBoundsError) {
    changes = await loadPersistedChangesFromPostgres(projectId, baseVersion)
  } else {
    throw err
  }
}
Defensive patterns

Strategy: fallback

Validate before calling

const persisted = await chunkStore.getProjectPersistedVersion(projectId); if (baseVersion <= persisted) { usePersistedPath() }

Type guard

function isVersionOutOfBounds(err) { return err instanceof VersionOutOfBoundsError }

Try / catch

try { return await chunkStore.getNonPersistedChanges(projectId, baseVersion) } catch (err) { if (isVersionOutOfBounds(err)) { return await loadFromPostgres(projectId, baseVersion) } throw err }

Prevention

When it happens

Trigger: Calling getNonPersistedChanges(projectId, baseVersion) where baseVersion is below the oldest retained non-persisted change, typically because a persist job already flushed those changes to postgres.

Common situations: A reader computing diff history against a base version that has just been persisted; background persistence racing an interactive read; asking for version 0 on a project whose history start has advanced.

Related errors


AI-assisted analysis of overleaf/overleaf@28ad3b03b7 (2026-09-03). Data as JSON: /api/errors/75e3d7990481a4ef. Report an issue: GitHub.