tursodatabase/turso · error
truncated MVCC logical log header: len={} header_size={}
Error message
truncated MVCC logical log header: len={} header_size={} What it means
scan_mvcc_log requires at least MVCC_LOG_HEADER_SIZE (56) bytes before validating anything. A .db-log that exists but is 1-55 bytes cannot hold a valid lml3 header and fails with HTTP 500. A zero-byte file is special-cased as an empty log, and a missing file triggers the replace-base fallback, so only short-but-nonempty files land here.
Source
Thrown at cli/sync_server.rs:951
return offset
.parse::<u64>()
.map_err(|err| anyhow!("invalid MVCC pull revision offset: {revision}: {err}"));
}
// Older page bootstrap responses from this test server used WAL frame
// numbers. Treat them as "the page snapshot already includes the current
// logical log" so the required follow-up logical pull becomes a no-op.
Ok(legacy_default)
}
fn scan_mvcc_log(log: &[u8]) -> Result<MvccLogSnapshot> {
if log.is_empty() {
return Ok(MvccLogSnapshot {
end_offset: 0,
crc_by_offset: vec![(0, 0)],
});
}
if log.len() < MVCC_LOG_HEADER_SIZE {
return Err(anyhow!(
"truncated MVCC logical log header: len={} header_size={}",
log.len(),
MVCC_LOG_HEADER_SIZE
));
}
validate_mvcc_log_header(log)?;
let mut running_crc = initial_mvcc_log_crc(log)?;
let mut offset = MVCC_LOG_HEADER_SIZE;
let mut crc_by_offset = vec![(MVCC_LOG_HEADER_SIZE as u64, running_crc)];
while offset < log.len() {
let Some((frame_end, frame_crc)) = read_mvcc_frame_boundary(log, offset, running_crc)?
else {
break;
};
running_crc = frame_crc;
offset = frame_end;
crc_by_offset.push((offset as u64, running_crc));View on GitHub (pinned to bad083fafb)
Solutions
- Move or delete the short .db-log — the server then takes the NotFound path and answers with replace-base pages so clients resync.
- Treat .db and .db-log as an atomic pair: recreate both together.
- If truncation recurs after crashes, investigate MVCC log write durability (the header should be written atomically or last).
Defensive patterns
Strategy: validation
Validate before calling
if let Ok(meta) = std::fs::metadata(&log_path) {
let len = meta.len() as usize;
if len > 0 && len < 56 {
// short-but-nonempty log will be rejected; remove it so the server
// falls back to replace-base pages
std::fs::remove_file(&log_path)?;
}
} Try / catch
On 500 'truncated MVCC logical log header', move the .db-log aside (server then serves replace-base pages), let clients re-bootstrap, and investigate why the write was interrupted.
Prevention
- Keep .db and .db-log as a pair; recreate them together.
- After crashes, check .db-log size before resuming logical pulls.
- Investigate recurring truncation as an MVCC log durability bug.
When it happens
Trigger: A crash-truncated first write of the log; a stray or manually created small file at the .db-log path; an incompatible writer that produced only a few bytes.
Common situations: Killing the process during log creation; leftover files from a different tool sharing the .db-log name; partially synced/copied file pairs.
Related errors
- invalid MVCC logical log magic
- invalid MVCC logical log header flags
- unsupported MVCC logical log version {}
- MVCC logical pull revision is from the future: client_offset
- MVCC logical pull offset is not a transaction boundary: {off
AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16).
Data as JSON: /api/errors/89e307cee8036acf.
Report an issue: GitHub.