benbjohnson/litestream · error

read page %d @ %d: %w

Error message

read page %d @ %d: %w

What it means

While building an LTX file from the database and WAL (snapshot path in writeLTXFromDB), litestream reads a page's bytes from the WAL file at the recorded frame offset. A ReadAt failure is wrapped as 'read page %d @ %d:' with the page number and byte offset so you know exactly which WAL frame could not be read.

Source

Thrown at db.go:2309

	for pgno := uint32(1); pgno <= commit; pgno++ {
		if pgno == lockPgno {
			continue
		}

		// Check if the caller has canceled during processing.
		select {
		case <-ctx.Done():
			return context.Cause(ctx)
		default:
		}

		// If page exists in the WAL, read from there.
		if offset, ok := pageMap[pgno]; ok {
			db.Logger.Log(ctx, internal.LevelTrace, "encode page from wal", "txid", enc.Header().MinTXID, "offset", offset, "pgno", pgno, "type", "db+wal")

			if n, err := walFile.ReadAt(data, offset+WALFrameHeaderSize); err != nil {
				return fmt.Errorf("read page %d @ %d: %w", pgno, offset, err)
			} else if n != len(data) {
				return fmt.Errorf("short read page %d @ %d", pgno, offset)
			}

			if err := enc.EncodePage(ltx.PageHeader{Pgno: pgno}, data); err != nil {
				return fmt.Errorf("encode ltx frame (pgno=%d): %w", pgno, err)
			}
			continue
		}

		offset := int64(pgno-1) * int64(db.pageSize)
		db.Logger.Log(ctx, internal.LevelTrace, "encode page from database", "offset", offset, "pgno", pgno)

		// Otherwise read directly from the database file.
		if _, err := db.f.ReadAt(data, offset); err != nil {
			return fmt.Errorf("read database page %d: %w", pgno, err)
		}
		if err := enc.EncodePage(ltx.PageHeader{Pgno: pgno}, data); err != nil {

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Ensure only one litestream process monitors the database (check for duplicate replicas/instances).
  2. Do not truncate, move, or delete the -wal file manually while replication is running.
  3. If the WAL is corrupt or was reset, run `litestream reset` for the database and let it rebuild from a replica or fresh snapshot.
  4. Check WAL file integrity and disk health if EIO is reported.

Example fix

// before
# two agents on one db
litestream replicate -config /etc/a.yml &
litestream replicate -config /etc/b.yml &   # second process corrupts state
// after
# single process per database
litestream replicate -config /etc/litestream.yml
Defensive patterns

Strategy: retry

Validate before calling

// ensure single writer
if pidFileLocked() { throw new Error('another litestream instance is running') }

Try / catch

if err := db.Sync(ctx); err != nil {
	if isWALReadErr(err) {
		// verify single-process ownership; consider litestream reset
	}
	return err
}

Prevention

When it happens

Trigger: walFile.ReadAt(data, offset+WALFrameHeaderSize) at db.go:2308 returned an error during snapshot LTX encoding: WAL truncated/shrunk concurrently by another checkpoint process, WAL deleted or rotated externally, or an I/O error on the WAL file.

Common situations: Two litestream processes (or litestream plus a manual checkpoint) racing on the same database; WAL reset via `litestream reset` or manual deletion mid-sync; corrupted or truncated WAL after a crash; NFS serving stale file size.

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/656b377f3e0bd80e. Report an issue: GitHub.