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
- Stop external checkpointing/WAL manipulation while litestream runs; do not copy hot database files.
- Restart litestream (or `litestream reset` for the DB) to rebuild the page map from the current WAL.
- Check whether another litestream version or process is checkpointing concurrently and remove the duplicate.
- 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
- Don't copy hot DB/WAL files while litestream runs; use the replica for backups.
- Suppress external PRAGMA wal_checkpoint(TRUNCATE) during replication.
- Detect stale WAL (salt mismatch) early and reset local state.
- Keep DB and WAL on the same healthy volume.
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
- read wal header after expected truncation: %w
- read page %d @ %d: %w
- snapshot interval must be greater than 0
- no snapshots available
- set synchronous: %w
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/1021c0e0cbd46733.
Report an issue: GitHub.