tursodatabase/turso · error

MVCC logical pull revision is from the future: client_offset

Error message

MVCC logical pull revision is from the future: client_offset={} server_offset={}

What it means

For MVCC logical pulls the server scans its on-disk .db-log and computes end_offset, the end of the last complete transaction. The client_revision (format g1:o<offset>) must parse to an offset <= end_offset; a larger offset means the client has seen a longer logical log than this server holds, so no incremental range can be served and the request fails with HTTP 500.

Source

Thrown at cli/sync_server.rs:699

        };
        let snapshot = match scan_mvcc_log(&log) {
            Ok(snapshot) => snapshot,
            Err(err) if is_nonportable_mvcc_log_error(&err) => {
                info!(
                    "logical pull requested but MVCC log is not portable; returning replace-base pages: {err}"
                );
                return self.handle_logical_fallback(
                    req,
                    fallback_revision,
                    &legacy_current_revision,
                    db_size,
                );
            }
            Err(err) => return Err(err),
        };
        let start_offset = parse_mvcc_revision_offset(&req.client_revision, snapshot.end_offset)?;
        if start_offset > snapshot.end_offset {
            return Err(anyhow!(
                "MVCC logical pull revision is from the future: client_offset={} server_offset={}",
                start_offset,
                snapshot.end_offset
            ));
        }
        let start = usize::try_from(start_offset)
            .map_err(|_| anyhow!("MVCC logical pull start offset overflows usize"))?;
        let end = usize::try_from(snapshot.end_offset)
            .map_err(|_| anyhow!("MVCC logical pull end offset overflows usize"))?;

        let mut response_body = Vec::new();
        let (mvcc_log, body) = if start == end {
            (None, Vec::new())
        } else {
            let crc_seed = if start_offset == 0 {
                None
            } else {
                let seed = snapshot.crc_seed_at(start_offset)?;

View on GitHub (pinned to bad083fafb)

Solutions

  1. Point the client back at the server whose logical log it actually synced from.
  2. Reset the client database (or clear its persisted revision) so it re-bootstraps from offset 0 against this server.
  3. If you control the server, answer future revisions with a ReplaceBase page response instead of a 500.

Example fix

// before: revision persisted from another server
req.client_revision = "g1:o9500".to_string();

// after: fresh bootstrap; server replies with its current revision
req.client_revision = String::new();
Defensive patterns

Strategy: fallback

Try / catch

On 500 containing 'revision is from the future', discard the persisted revision, re-bootstrap the local database, and pull once with an empty client_revision; retrying the same revision will always fail.

Prevention

When it happens

Trigger: A client synced from a different (or newer production) server whose persisted revision g1:oN has N greater than this server's end_offset; the server's .db/.db-log recreated or restored from an older snapshot while the client kept its revision.

Common situations: Swapping sync endpoints in tests; restoring a server database from backup while reusing client state; running one client against two server instances.

Related errors


AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16). Data as JSON: /api/errors/1bc0cd0fe8aecc9e. Report an issue: GitHub.