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

  1. Ensure only one process writes the SQLite database (litestream assumes it owns replication)
  2. Check the wrapped inner cause: WAL read errors vs LTX decode errors need different fixes
  3. 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

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


AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06). Data as JSON: /api/errors/9649ad11fae1c109. Report an issue: GitHub.