benbjohnson/litestream · error
last page match: %w
Error message
last page match: %w
What it means
Wraps any error from db.lastPageMatch(), which reads the WAL page at the previously synced offset and checks that the same page exists in the last LTX file. If this check fails, litestream cannot prove the WAL wasn't overwritten by another process and conservatively snapshots.
Source
Thrown at db.go:1790
prevWALOffset := info.offset - frameSize
db.Logger.Debug("verify", "saltMatch", saltMatch, "prevWALOffset", prevWALOffset)
if prevWALOffset == WALHeaderSize {
if saltMatch { // No writes occurred since last sync, salt still matches
info.snapshotting = false
return info, nil
}
// Salt has changed but we don't know if writes occurred since last sync
info.reason = "wal header salt reset, snapshotting"
return info, nil
} else if prevWALOffset < WALHeaderSize {
return info, fmt.Errorf("prev WAL offset is less than the header size: %d", prevWALOffset)
}
// If we can't verify the last page is in the last LTX file, then we need to snapshot.
lastPageMatch, err := db.lastPageMatch(ctx, dec, prevWALOffset, frameSize)
if err != nil {
return info, fmt.Errorf("last page match: %w", err)
} else if !lastPageMatch {
info.reason = "last page does not exist in last ltx file, wal overwritten by another process"
return info, nil
}
db.Logger.Debug("verify.2", "lastPageMatch", lastPageMatch)
// Salt has changed which could indicate a FULL checkpoint.
// If we have a last page match, then we can assume that the WAL has not been overwritten.
if !saltMatch {
db.Logger.Log(ctx, internal.LevelTrace, "wal restarted",
"salt1", salt1,
"salt2", salt2)
info.offset = WALHeaderSize
info.salt1, info.salt2 = salt1, salt2
if detected, err := db.detectFullCheckpoint(ctx, [][2]uint32{{salt1, salt2}, {dec.Header().WALSalt1, dec.Header().WALSalt2}}); err != nil {View on GitHub (pinned to 4ed7a308f6)
Solutions
- Ensure only one process writes the SQLite database (litestream assumes it owns replication)
- Check the wrapped inner cause: WAL read errors vs LTX decode errors need different fixes
- If state is unrecoverable, `litestream reset` to force a clean snapshot
Defensive patterns
Strategy: try-catch
Validate before calling
// verify single-writer assumption and WAL readability before sync
if _, err := os.Stat(dbPath + "-wal"); err != nil { /* fix before syncing */ } Try / catch
if err := db.Sync(ctx); err != nil {
if strings.Contains(err.Error(), "last page match") {
// unwrap cause: WAL read vs LTX decode; snapshot fallback will kick in on next cycle
}
} Prevention
- Only one process should write the SQLite DB
- Watch for 'wal overwritten by another process' reasons in logs
- Keep LTX files on healthy storage
When it happens
Trigger: lastPageMatch returns an error — typically the underlying readWALFileAt or LTX page decode failing (see errors 278/279) while comparing WAL vs LTX content.
Common situations: WAL being rewritten concurrently by another SQLite writer on the same file; IO errors; corrupted LTX payloads.
Understand the failure class
Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.
Related errors
- decode
- prev WAL offset is less than the header size: %d
- write ltx from wal: %w
- invalid replica, checksum mismatch
- ltx file corrupted
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/9649ad11fae1c109.
Report an issue: GitHub.