benbjohnson/litestream · error

short read page %d @ %d

Error message

short read page %d @ %d

What it means

ReadAt returned fewer bytes than a full database page from the WAL frame at the given offset. Litestream treats a partial page read as a hard error ('short read page %d @ %d') because an LTX frame must contain a complete page; a truncated frame means the WAL's page map no longer matches the file contents.

Source

Thrown at db.go:2311

		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 {
			return fmt.Errorf("encode ltx frame (pgno=%d): %w", pgno, err)
		}

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Stop external checkpointing/WAL manipulation while litestream runs; do not copy hot database files.
  2. Restart litestream (or `litestream reset` for the DB) to rebuild the page map from the current WAL.
  3. Check whether another litestream version or process is checkpointing concurrently and remove the duplicate.
  4. If the WAL is physically truncated by a crash, restore from a replica and re-seed.
Defensive patterns

Strategy: retry

Try / catch

if err := db.Sync(ctx); err != nil {
	if strings.Contains(err.Error(), "short read page") {
		// WAL changed under us: stop external writers, reset/rebuild
	}
	return err
}

Prevention

When it happens

Trigger: walFile.ReadAt at db.go:2310 returned n != pageSize during snapshot encoding: the WAL was truncated or overwritten between building the pageMap and reading frames, an offset in pageMap points past the current WAL end (stale map after external WAL modification), or the WAL file is corrupt/truncated.

Common situations: External processes running `PRAGMA wal_checkpoint(TRUNCATE)` or deleting the WAL mid-snapshot; stale salt/frame mismatch after WAL reset (issue #927 family); crashed writer leaving a partially written WAL; copying DB+WAL files while litestream runs.

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/1021c0e0cbd46733. Report an issue: GitHub.